原材料入库结存
This commit is contained in:
@@ -10,6 +10,7 @@ using YY.Admin.EventBus;
|
||||
using YY.Admin.Filter;
|
||||
using YY.Admin.Module;
|
||||
using YY.Admin.Properties;
|
||||
using YY.Admin.Services.Service.Print;
|
||||
using YY.Admin.Setup;
|
||||
using YY.Admin.ViewModels;
|
||||
using YY.Admin.Views;
|
||||
@@ -96,6 +97,9 @@ namespace YY.Admin
|
||||
|
||||
_logger.Information("应用程序已启动");
|
||||
|
||||
// 加载 PrintDot 本地设置(使 PrintDotService 在任何页面调用前已有配置)
|
||||
PrintDotSettings.Load();
|
||||
|
||||
// 启动断联续传同步模块
|
||||
_syncModule.OnInitialized(Container);
|
||||
}
|
||||
|
||||
@@ -162,6 +162,14 @@ public class StompWebSocketService : ISignalRService
|
||||
await SendFrameAsync(
|
||||
BuildSubscribeFrame("sub-mes-warehouse-areas", "/topic/sync/mes-warehouse-areas"),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
// 打印模板变更:订阅 /topic/sync/print-templates
|
||||
await SendFrameAsync(
|
||||
BuildSubscribeFrame("sub-print-templates", "/topic/sync/print-templates"),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
// 业务打印绑定变更:订阅 /topic/sync/print-biz-binds
|
||||
await SendFrameAsync(
|
||||
BuildSubscribeFrame("sub-print-biz-binds", "/topic/sync/print-biz-binds"),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// 订阅服务端 PONG 回复(应用层假在线检测)
|
||||
await SendFrameAsync(
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
using Microsoft.Web.WebView2.Wpf;
|
||||
|
||||
namespace YY.Admin.Infrastructure.Print;
|
||||
|
||||
/// <summary>
|
||||
/// 使用隐藏 WebView2 窗口将 HTML 字符串渲染为 PDF base64。
|
||||
/// 所有调用必须最终在 WPF UI 线程执行,由内部 Dispatcher 保证。
|
||||
/// </summary>
|
||||
public static class HtmlToPdfRenderer
|
||||
{
|
||||
public static Task<string> RenderAsync(string html, double widthMm = 210, double heightMm = 297)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
Application.Current.Dispatcher.InvokeAsync(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await RenderOnUiThreadAsync(html, widthMm, heightMm);
|
||||
tcs.SetResult(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.SetException(ex);
|
||||
}
|
||||
});
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private static async Task<string> RenderOnUiThreadAsync(string html, double widthMm, double heightMm)
|
||||
{
|
||||
var tempHtml = Path.ChangeExtension(Path.GetTempFileName(), ".html");
|
||||
var tempPdf = Path.ChangeExtension(Path.GetTempFileName(), ".pdf");
|
||||
Window? win = null;
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllText(tempHtml, html, Encoding.UTF8);
|
||||
|
||||
var env = await CoreWebView2Environment.CreateAsync();
|
||||
var wv = new WebView2();
|
||||
|
||||
win = new Window
|
||||
{
|
||||
Width = 1,
|
||||
Height = 1,
|
||||
Left = -99999,
|
||||
Top = -99999,
|
||||
ShowInTaskbar = false,
|
||||
WindowStyle = WindowStyle.None,
|
||||
Visibility = Visibility.Visible
|
||||
};
|
||||
win.Content = wv;
|
||||
win.Show();
|
||||
|
||||
await wv.EnsureCoreWebView2Async(env);
|
||||
|
||||
var navTcs = new TaskCompletionSource();
|
||||
wv.CoreWebView2.NavigationCompleted += (_, _) => navTcs.TrySetResult();
|
||||
wv.CoreWebView2.Navigate(new Uri(tempHtml).AbsoluteUri);
|
||||
await navTcs.Task;
|
||||
|
||||
// 短暂等待 JS/CSS 完成渲染
|
||||
await Task.Delay(200);
|
||||
|
||||
var settings = env.CreatePrintSettings();
|
||||
settings.PageWidth = widthMm / 25.4;
|
||||
settings.PageHeight = heightMm / 25.4;
|
||||
settings.MarginTop = 0;
|
||||
settings.MarginBottom = 0;
|
||||
settings.MarginLeft = 0;
|
||||
settings.MarginRight = 0;
|
||||
settings.ShouldPrintHeaderAndFooter = false;
|
||||
settings.ShouldPrintBackgrounds = true;
|
||||
|
||||
await wv.CoreWebView2.PrintToPdfAsync(tempPdf, settings);
|
||||
|
||||
var pdfBytes = File.ReadAllBytes(tempPdf);
|
||||
return Convert.ToBase64String(pdfBytes);
|
||||
}
|
||||
finally
|
||||
{
|
||||
win?.Close();
|
||||
try { File.Delete(tempHtml); } catch { }
|
||||
try { File.Delete(tempPdf); } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ using YY.Admin.Views.WeightRecord;
|
||||
using YY.Admin.Views.RawMaterialCard;
|
||||
using YY.Admin.Views.WarehouseArea;
|
||||
using YY.Admin.Views.RawMaterialEntry;
|
||||
using YY.Admin.Views.Print;
|
||||
|
||||
namespace YY.Admin
|
||||
{
|
||||
@@ -81,6 +82,12 @@ namespace YY.Admin
|
||||
containerRegistry.RegisterForNavigation<RawMaterialCardListView>();
|
||||
// 库区管理
|
||||
containerRegistry.RegisterForNavigation<WarehouseAreaListView>();
|
||||
// 打印设置
|
||||
containerRegistry.RegisterForNavigation<PrintSettingsView>();
|
||||
// 打印模板列表
|
||||
containerRegistry.RegisterForNavigation<PrintTemplateListView>();
|
||||
// 业务打印绑定(只读缓存)
|
||||
containerRegistry.RegisterForNavigation<PrintBizTemplateBindListView>();
|
||||
}
|
||||
}
|
||||
public class DialogWindow : Window, IDialogWindow
|
||||
|
||||
@@ -25,6 +25,7 @@ using YY.Admin.Services.Service.Vehicle;
|
||||
using YY.Admin.Services.Service.Warehouse;
|
||||
using YY.Admin.Services.Service.WarehouseArea;
|
||||
using YY.Admin.Services.Service.WeightRecord;
|
||||
using YY.Admin.Services.Service.Print;
|
||||
|
||||
namespace YY.Admin.Module;
|
||||
|
||||
@@ -74,6 +75,12 @@ public class SyncModule : IModule
|
||||
containerRegistry.RegisterSingleton<DictSyncCoordinator>();
|
||||
// 统一轮询管理器(修改 SyncPollManager.PollInterval 即可调整所有模块的轮询间隔)
|
||||
containerRegistry.RegisterSingleton<SyncPollManager>();
|
||||
// 打印服务:PrintDot 桥接器 + 打印模板(含 STOMP 实时同步 + 本地缓存)
|
||||
containerRegistry.RegisterSingleton<IPrintDotService, PrintDotService>();
|
||||
containerRegistry.RegisterSingleton<IPrintTemplateService, PrintTemplateService>();
|
||||
containerRegistry.RegisterSingleton<IPrintBizTemplateBindService, PrintBizTemplateBindService>();
|
||||
containerRegistry.RegisterSingleton<PrintTemplateSyncCoordinator>();
|
||||
containerRegistry.RegisterSingleton<PrintBizTemplateBindSyncCoordinator>();
|
||||
|
||||
var serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddTransient<DisconnectGuardHandler>();
|
||||
@@ -140,6 +147,9 @@ public class SyncModule : IModule
|
||||
_ = containerProvider.Resolve<CategorySyncCoordinator>();
|
||||
// 强制实例化数据字典同步协调器
|
||||
_ = containerProvider.Resolve<DictSyncCoordinator>();
|
||||
// 强制实例化打印模板同步协调器
|
||||
_ = containerProvider.Resolve<PrintTemplateSyncCoordinator>();
|
||||
_ = containerProvider.Resolve<PrintBizTemplateBindSyncCoordinator>();
|
||||
}
|
||||
|
||||
private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
|
||||
|
||||
@@ -5,15 +5,27 @@ namespace YY.Admin.Module
|
||||
{
|
||||
public class TabSource : BindableBase
|
||||
{
|
||||
private string? _name;
|
||||
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
public virtual string? Name { get; set; }
|
||||
public virtual string? Name
|
||||
{
|
||||
get => _name;
|
||||
set => SetProperty(ref _name, value);
|
||||
}
|
||||
|
||||
private string? _icon;
|
||||
|
||||
/// <summary>
|
||||
/// 图标
|
||||
/// </summary>
|
||||
public virtual string? Icon { get; set; }
|
||||
public virtual string? Icon
|
||||
{
|
||||
get => _icon;
|
||||
set => SetProperty(ref _icon, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 图标类型
|
||||
|
||||
@@ -140,7 +140,23 @@ namespace YY.Admin.ViewModels.Control
|
||||
// 已实现页面:库区管理
|
||||
["WarehouseAreaListView"] = "WarehouseAreaListView",
|
||||
["/xslmes/mesXslWarehouseArea"] = "WarehouseAreaListView",
|
||||
["mesXslWarehouseArea"] = "WarehouseAreaListView"
|
||||
["mesXslWarehouseArea"] = "WarehouseAreaListView",
|
||||
|
||||
// 已实现页面:打印设置
|
||||
["PrintSettingsView"] = "PrintSettingsView",
|
||||
["/system/printSettings"] = "PrintSettingsView",
|
||||
["printSettings"] = "PrintSettingsView",
|
||||
|
||||
// 已实现页面:打印模板
|
||||
["PrintTemplateListView"] = "PrintTemplateListView",
|
||||
["/platform/print"] = "PrintTemplateListView",
|
||||
["print"] = "PrintTemplateListView",
|
||||
["printTemplate"] = "PrintTemplateListView",
|
||||
|
||||
// 已实现页面:业务打印绑定(桌面端)
|
||||
["PrintBizTemplateBindListView"] = "PrintBizTemplateBindListView",
|
||||
["/platform/printBizBind"] = "PrintBizTemplateBindListView",
|
||||
["printBizBind"] = "PrintBizTemplateBindListView"
|
||||
};
|
||||
|
||||
private MenuItem? _selectedMenuItem;
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.Extensions.Configuration;
|
||||
using YY.Admin.Core.Helper;
|
||||
using YY.Admin.Core.Session;
|
||||
using YY.Admin.FluentValidation;
|
||||
using YY.Admin.Helper;
|
||||
using YY.Admin.Services;
|
||||
using YY.Admin.Services.Service.Auth;
|
||||
using YY.Admin.Services.Service.Jeecg;
|
||||
@@ -220,6 +221,31 @@ namespace YY.Admin.ViewModels
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
bool connected = false;
|
||||
var settings = ServerSettingsStore.Load();
|
||||
if (settings.DisconnectConnection)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
IsBackendConnected = false;
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略窗口关闭后的调度异常
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(ConnectivityCheckIntervalSeconds), cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
try
|
||||
{
|
||||
// 每轮都重新读取配置,保存服务器设置后可即时生效
|
||||
@@ -269,6 +295,11 @@ namespace YY.Admin.ViewModels
|
||||
{
|
||||
if (r.Result == ButtonResult.OK)
|
||||
{
|
||||
var settings = ServerSettingsStore.Load();
|
||||
if (settings.DisconnectConnection)
|
||||
{
|
||||
IsBackendConnected = false;
|
||||
}
|
||||
LoginMessage = "服务器配置已保存";
|
||||
}
|
||||
});
|
||||
|
||||
@@ -165,6 +165,18 @@ namespace YY.Admin.ViewModels
|
||||
set => SetProperty(ref _isAppSettingsOpen, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为 true 时隐藏左侧功能菜单树(次级侧栏)
|
||||
/// </summary>
|
||||
private bool _isMenuTreePanelCollapsed;
|
||||
public bool IsMenuTreePanelCollapsed
|
||||
{
|
||||
get => _isMenuTreePanelCollapsed;
|
||||
set => SetProperty(ref _isMenuTreePanelCollapsed, value);
|
||||
}
|
||||
|
||||
private NavItem? _menuTreeToggleNavItem;
|
||||
|
||||
private AppSettingsViewModel? _appSettingsViewModel;
|
||||
public AppSettingsViewModel? AppSettingsViewModel {
|
||||
get => _appSettingsViewModel;
|
||||
@@ -175,6 +187,7 @@ namespace YY.Admin.ViewModels
|
||||
public ICommand ResetAppSettingsCommand { get; }
|
||||
public ICommand OpenAppSettingsCommand { get; }
|
||||
public ICommand OpenServerSettingsCommand { get; }
|
||||
public ICommand ToggleMenuTreePanelCommand { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -212,6 +225,7 @@ namespace YY.Admin.ViewModels
|
||||
OpenAppSettingsCommand = new DelegateCommand(OpenAppSettings);
|
||||
ResetAppSettingsCommand = new DelegateCommand(ResetAppSettings);
|
||||
OpenServerSettingsCommand = new DelegateCommand(OpenServerSettings);
|
||||
ToggleMenuTreePanelCommand = new DelegateCommand(ToggleMenuTreePanel);
|
||||
|
||||
// 初始化Sidebar数据
|
||||
InitNavItems();
|
||||
@@ -300,6 +314,13 @@ namespace YY.Admin.ViewModels
|
||||
Name = "Tab区域",
|
||||
Command = new DelegateCommand<NavItem>(OnOpenOrActivateTab)
|
||||
},
|
||||
(_menuTreeToggleNavItem = new NavItem {
|
||||
Icon = "ChevronDoubleLeft",
|
||||
Name = "折叠菜单",
|
||||
AlignBottom = true,
|
||||
IsActive = false,
|
||||
Command = ToggleMenuTreePanelCommand
|
||||
}),
|
||||
new NavItem {
|
||||
Icon = "Server",
|
||||
Name = "服务器设置",
|
||||
@@ -678,6 +699,29 @@ namespace YY.Admin.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 折叠 / 展示左侧菜单树区域
|
||||
/// </summary>
|
||||
private void ToggleMenuTreePanel()
|
||||
{
|
||||
IsMenuTreePanelCollapsed = !IsMenuTreePanelCollapsed;
|
||||
if (_menuTreeToggleNavItem == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsMenuTreePanelCollapsed)
|
||||
{
|
||||
_menuTreeToggleNavItem.Name = "展示菜单";
|
||||
_menuTreeToggleNavItem.Icon = "ChevronDoubleRight";
|
||||
}
|
||||
else
|
||||
{
|
||||
_menuTreeToggleNavItem.Name = "折叠菜单";
|
||||
_menuTreeToggleNavItem.Icon = "ChevronDoubleLeft";
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenServerSettings()
|
||||
{
|
||||
_dialogService.ShowDialog("ServerSettingsDialog", r =>
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows;
|
||||
using Prism.Commands;
|
||||
using Prism.Events;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Core.Entity;
|
||||
using YY.Admin.Core.Events;
|
||||
using YY.Admin.Core.Services;
|
||||
using YY.Admin.Views.Print;
|
||||
|
||||
namespace YY.Admin.ViewModels.Print;
|
||||
|
||||
/// <summary>业务打印绑定列表(只读,本地缓存 + 同步)</summary>
|
||||
public class PrintBizTemplateBindListViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IPrintBizTemplateBindService _bindService;
|
||||
private SubscriptionToken? _changeToken;
|
||||
|
||||
private List<PrintBizTemplateBind> _all = new();
|
||||
|
||||
public ObservableCollection<PrintBizTemplateBind> Items { get; } = new();
|
||||
|
||||
private string _statusMessage = string.Empty;
|
||||
public string StatusMessage
|
||||
{
|
||||
get => _statusMessage;
|
||||
set => SetProperty(ref _statusMessage, value);
|
||||
}
|
||||
|
||||
private string? _filterBizCode;
|
||||
public string? FilterBizCode
|
||||
{
|
||||
get => _filterBizCode;
|
||||
set => SetProperty(ref _filterBizCode, value);
|
||||
}
|
||||
|
||||
private string? _filterBizName;
|
||||
public string? FilterBizName
|
||||
{
|
||||
get => _filterBizName;
|
||||
set => SetProperty(ref _filterBizName, value);
|
||||
}
|
||||
|
||||
private string? _filterTemplateCode;
|
||||
public string? FilterTemplateCode
|
||||
{
|
||||
get => _filterTemplateCode;
|
||||
set => SetProperty(ref _filterTemplateCode, value);
|
||||
}
|
||||
|
||||
public DelegateCommand SearchCommand { get; }
|
||||
public DelegateCommand ResetCommand { get; }
|
||||
public DelegateCommand<PrintBizTemplateBind> DetailCommand { get; }
|
||||
|
||||
public PrintBizTemplateBindListViewModel(
|
||||
IPrintBizTemplateBindService bindService,
|
||||
IContainerExtension container,
|
||||
IRegionManager regionManager) : base(container, regionManager)
|
||||
{
|
||||
_bindService = bindService;
|
||||
|
||||
SearchCommand = new DelegateCommand(ApplyFilter);
|
||||
ResetCommand = new DelegateCommand(() =>
|
||||
{
|
||||
FilterBizCode = null;
|
||||
FilterBizName = null;
|
||||
FilterTemplateCode = null;
|
||||
ApplyFilter();
|
||||
});
|
||||
DetailCommand = new DelegateCommand<PrintBizTemplateBind>(OpenDetail);
|
||||
|
||||
_changeToken = _eventAggregator
|
||||
.GetEvent<PrintBizTemplateBindChangedEvent>()
|
||||
.Subscribe(_ => { RefreshSilentlyAsync().ConfigureAwait(false); }, ThreadOption.UIThread);
|
||||
|
||||
ShowCached();
|
||||
_ = RefreshSilentlyAsync();
|
||||
}
|
||||
|
||||
private void ShowCached()
|
||||
{
|
||||
var cached = _bindService.GetCached();
|
||||
if (cached.Count == 0) return;
|
||||
_all = cached.ToList();
|
||||
ApplyFilter();
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
private async Task RefreshSilentlyAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var list = await _bindService.RefreshCacheAsync().ConfigureAwait(false);
|
||||
await Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
_all = list.ToList();
|
||||
ApplyFilter();
|
||||
UpdateStatus();
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
var cached = _bindService.GetCached();
|
||||
if (cached.Count > 0 && _all.Count == 0)
|
||||
{
|
||||
await Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
_all = cached.ToList();
|
||||
ApplyFilter();
|
||||
UpdateStatus();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyFilter()
|
||||
{
|
||||
IEnumerable<PrintBizTemplateBind> result = _all;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(FilterBizCode))
|
||||
result = result.Where(x => (x.BizCode ?? string.Empty)
|
||||
.Contains(FilterBizCode, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(FilterBizName))
|
||||
result = result.Where(x => (x.BizName ?? string.Empty)
|
||||
.Contains(FilterBizName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(FilterTemplateCode))
|
||||
result = result.Where(x => (x.TemplateCode ?? string.Empty)
|
||||
.Contains(FilterTemplateCode, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var filtered = result.ToList();
|
||||
|
||||
for (int i = Items.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (!filtered.Any(t => t.Id == Items[i].Id))
|
||||
Items.RemoveAt(i);
|
||||
}
|
||||
for (int i = 0; i < filtered.Count; i++)
|
||||
{
|
||||
var item = filtered[i];
|
||||
var existingIdx = -1;
|
||||
for (int j = 0; j < Items.Count; j++)
|
||||
{
|
||||
if (Items[j].Id == item.Id) { existingIdx = j; break; }
|
||||
}
|
||||
if (existingIdx < 0)
|
||||
Items.Insert(i, item);
|
||||
else
|
||||
{
|
||||
if (existingIdx != i) Items.Move(existingIdx, i);
|
||||
Items[i] = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateStatus()
|
||||
{
|
||||
var hasFilter = !string.IsNullOrWhiteSpace(FilterBizCode)
|
||||
|| !string.IsNullOrWhiteSpace(FilterBizName)
|
||||
|| !string.IsNullOrWhiteSpace(FilterTemplateCode);
|
||||
StatusMessage = hasFilter
|
||||
? $"筛选结果 {Items.Count} / {_all.Count} 条"
|
||||
: _all.Count > 0
|
||||
? $"共 {_all.Count} 条绑定(缓存于本地,可离线查看)"
|
||||
: "暂无数据(联网后将自动同步)";
|
||||
}
|
||||
|
||||
private async void OpenDetail(PrintBizTemplateBind? row)
|
||||
{
|
||||
if (row == null) return;
|
||||
PrintBizTemplateBind model = row;
|
||||
try
|
||||
{
|
||||
var fresh = await _bindService.GetByIdAsync(row.Id ?? "").ConfigureAwait(false);
|
||||
if (fresh != null) model = fresh;
|
||||
}
|
||||
catch { /* 使用列表行数据 */ }
|
||||
|
||||
// 网络请求 continuation 可能在非 STA 线程,Window 必须在 UI 线程创建
|
||||
var app = Application.Current;
|
||||
if (app?.Dispatcher == null) return;
|
||||
await app.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var win = new PrintBizTemplateBindDetailWindow(model)
|
||||
{
|
||||
Owner = app.MainWindow
|
||||
};
|
||||
win.ShowDialog();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows;
|
||||
using Prism.Commands;
|
||||
using YY.Admin.Core.Entity;
|
||||
using YY.Admin.Core.Services;
|
||||
using YY.Admin.Services.Service.Print;
|
||||
using YY.Admin.Core;
|
||||
|
||||
namespace YY.Admin.ViewModels.Print;
|
||||
|
||||
public class PrintSettingsViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IPrintDotService _printDotService;
|
||||
private readonly IPrintTemplateService _printTemplateService;
|
||||
|
||||
// ── 连接设置 ──────────────────────────────────────────────────────────
|
||||
|
||||
private string _wsUrl = "ws://127.0.0.1:1122/ws";
|
||||
public string WsUrl
|
||||
{
|
||||
get => _wsUrl;
|
||||
set => SetProperty(ref _wsUrl, value);
|
||||
}
|
||||
|
||||
// ── 打印机列表 ──────────────────────────────────────────────────────────
|
||||
|
||||
public ObservableCollection<PrintDotPrinter> Printers { get; } = new();
|
||||
|
||||
private PrintDotPrinter? _selectedPrinter;
|
||||
public PrintDotPrinter? SelectedPrinter
|
||||
{
|
||||
get => _selectedPrinter;
|
||||
set => SetProperty(ref _selectedPrinter, value);
|
||||
}
|
||||
|
||||
// ── 模板列表 ────────────────────────────────────────────────────────────
|
||||
|
||||
public ObservableCollection<PrintTemplate> Templates { get; } = new();
|
||||
|
||||
// ── 状态 ────────────────────────────────────────────────────────────────
|
||||
|
||||
private bool _isBusy;
|
||||
public bool IsBusy
|
||||
{
|
||||
get => _isBusy;
|
||||
set => SetProperty(ref _isBusy, value);
|
||||
}
|
||||
|
||||
private string _statusMessage = string.Empty;
|
||||
public string StatusMessage
|
||||
{
|
||||
get => _statusMessage;
|
||||
set => SetProperty(ref _statusMessage, value);
|
||||
}
|
||||
|
||||
// ── 命令 ────────────────────────────────────────────────────────────────
|
||||
|
||||
public DelegateCommand TestConnectionCommand { get; }
|
||||
public DelegateCommand SaveCommand { get; }
|
||||
public DelegateCommand RefreshTemplatesCommand { get; }
|
||||
|
||||
public PrintSettingsViewModel(
|
||||
IPrintDotService printDotService,
|
||||
IPrintTemplateService printTemplateService,
|
||||
IContainerExtension container,
|
||||
IRegionManager regionManager) : base(container, regionManager)
|
||||
{
|
||||
_printDotService = printDotService;
|
||||
_printTemplateService = printTemplateService;
|
||||
|
||||
TestConnectionCommand = new DelegateCommand(async () => await TestConnectionAsync());
|
||||
SaveCommand = new DelegateCommand(SaveSettings);
|
||||
RefreshTemplatesCommand = new DelegateCommand(async () => await RefreshTemplatesAsync());
|
||||
|
||||
// 加载已保存的设置
|
||||
var saved = PrintDotSettings.Load();
|
||||
WsUrl = saved.WsUrl;
|
||||
if (!string.IsNullOrWhiteSpace(saved.SelectedPrinter))
|
||||
_selectedPrinter = new PrintDotPrinter(saved.SelectedPrinter, false);
|
||||
}
|
||||
|
||||
private async Task TestConnectionAsync()
|
||||
{
|
||||
IsBusy = true;
|
||||
StatusMessage = "正在连接...";
|
||||
Printers.Clear();
|
||||
try
|
||||
{
|
||||
// 临时用当前输入的 URL 测试
|
||||
PrintDotSettings.Current!.WsUrl = WsUrl;
|
||||
var list = await _printDotService.GetPrintersAsync();
|
||||
foreach (var p in list) Printers.Add(p);
|
||||
|
||||
// 还原保存的已选打印机
|
||||
var saved = PrintDotSettings.Load();
|
||||
var match = list.FirstOrDefault(p => p.Name == saved.SelectedPrinter);
|
||||
SelectedPrinter = match ?? (list.Count > 0 ? list[0] : null);
|
||||
|
||||
StatusMessage = $"连接成功,共 {list.Count} 台打印机";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"连接失败:{ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveSettings()
|
||||
{
|
||||
var settings = new PrintDotSettings
|
||||
{
|
||||
WsUrl = WsUrl.Trim(),
|
||||
SelectedPrinter = SelectedPrinter?.Name ?? string.Empty
|
||||
};
|
||||
settings.Save();
|
||||
StatusMessage = "设置已保存";
|
||||
MessageBox.Show("PrintDot 设置已保存", "保存成功", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
|
||||
private async Task RefreshTemplatesAsync()
|
||||
{
|
||||
IsBusy = true;
|
||||
Templates.Clear();
|
||||
try
|
||||
{
|
||||
var list = await _printTemplateService.ListAsync();
|
||||
foreach (var t in list) Templates.Add(t);
|
||||
StatusMessage = $"已加载 {list.Count} 个打印模板";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"加载模板失败:{ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows;
|
||||
using Prism.Commands;
|
||||
using Prism.Events;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Core.Entity;
|
||||
using YY.Admin.Core.Events;
|
||||
using YY.Admin.Core.Services;
|
||||
using YY.Admin.Services.Service.Print;
|
||||
using YY.Admin.Views.Print;
|
||||
|
||||
namespace YY.Admin.ViewModels.Print;
|
||||
|
||||
public class PrintTemplateListViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IPrintTemplateService _printTemplateService;
|
||||
private readonly IPrintDotService _printDotService;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private SubscriptionToken? _changeToken;
|
||||
|
||||
private List<PrintTemplate> _allTemplates = new();
|
||||
|
||||
public ObservableCollection<PrintTemplate> Templates { get; } = new();
|
||||
|
||||
// ── PrintDot 打印机选择 ────────────────────────────────────────────────
|
||||
public ObservableCollection<PrintDotPrinter> Printers { get; } = new();
|
||||
|
||||
private bool _suppressPrinterSave;
|
||||
private PrintDotPrinter? _selectedPrinter;
|
||||
public PrintDotPrinter? SelectedPrinter
|
||||
{
|
||||
get => _selectedPrinter;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _selectedPrinter, value)) return;
|
||||
if (_suppressPrinterSave) return;
|
||||
// 持久化用户选择,预览窗口和后续会话使用
|
||||
var s = PrintDotSettings.Load();
|
||||
s.SelectedPrinter = value?.Name ?? string.Empty;
|
||||
s.Save();
|
||||
}
|
||||
}
|
||||
|
||||
private string _printerStatus = string.Empty;
|
||||
public string PrinterStatus
|
||||
{
|
||||
get => _printerStatus;
|
||||
set => SetProperty(ref _printerStatus, value);
|
||||
}
|
||||
|
||||
private string _statusMessage = string.Empty;
|
||||
public string StatusMessage
|
||||
{
|
||||
get => _statusMessage;
|
||||
set => SetProperty(ref _statusMessage, value);
|
||||
}
|
||||
|
||||
private string? _filterCode;
|
||||
public string? FilterCode
|
||||
{
|
||||
get => _filterCode;
|
||||
set => SetProperty(ref _filterCode, value);
|
||||
}
|
||||
|
||||
private string? _filterName;
|
||||
public string? FilterName
|
||||
{
|
||||
get => _filterName;
|
||||
set => SetProperty(ref _filterName, value);
|
||||
}
|
||||
|
||||
private string? _filterCategory;
|
||||
public string? FilterCategory
|
||||
{
|
||||
get => _filterCategory;
|
||||
set => SetProperty(ref _filterCategory, value);
|
||||
}
|
||||
|
||||
public DelegateCommand SearchCommand { get; }
|
||||
public DelegateCommand ResetCommand { get; }
|
||||
public DelegateCommand<PrintTemplate> PreviewCommand { get; }
|
||||
public DelegateCommand RefreshPrintersCommand { get; }
|
||||
|
||||
public PrintTemplateListViewModel(
|
||||
IPrintTemplateService printTemplateService,
|
||||
IPrintDotService printDotService,
|
||||
IEventAggregator eventAggregator,
|
||||
IContainerExtension container,
|
||||
IRegionManager regionManager) : base(container, regionManager)
|
||||
{
|
||||
_printTemplateService = printTemplateService;
|
||||
_printDotService = printDotService;
|
||||
_eventAggregator = eventAggregator;
|
||||
|
||||
SearchCommand = new DelegateCommand(ApplyFilter);
|
||||
PreviewCommand = new DelegateCommand<PrintTemplate>(ShowPreview);
|
||||
ResetCommand = new DelegateCommand(() =>
|
||||
{
|
||||
FilterCode = null;
|
||||
FilterName = null;
|
||||
FilterCategory = null;
|
||||
ApplyFilter();
|
||||
});
|
||||
RefreshPrintersCommand = new DelegateCommand(async () => await RefreshPrintersAsync(verbose: true));
|
||||
|
||||
_changeToken = _eventAggregator
|
||||
.GetEvent<PrintTemplateChangedEvent>()
|
||||
.Subscribe(_ => { RefreshSilentlyAsync().ConfigureAwait(false); }, ThreadOption.UIThread);
|
||||
|
||||
// 先用缓存立即填充,再后台静默刷新
|
||||
ShowCached();
|
||||
_ = RefreshSilentlyAsync();
|
||||
|
||||
// 后台静默连接 PrintDot 桥接器,初次加载打印机
|
||||
_ = RefreshPrintersAsync(verbose: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 PrintDot 桥接器拉取打印机列表,与后端 web 列表页的 fetchPrintDotPrinters 行为对齐。
|
||||
/// </summary>
|
||||
private async Task RefreshPrintersAsync(bool verbose)
|
||||
{
|
||||
if (verbose) PrinterStatus = "刷新打印机中...";
|
||||
var savedName = PrintDotSettings.Load().SelectedPrinter;
|
||||
try
|
||||
{
|
||||
var list = await _printDotService.GetPrintersAsync();
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
Printers.Clear();
|
||||
foreach (var p in list) Printers.Add(p);
|
||||
|
||||
// 选中规则:上次保存 > 系统默认 > 首台
|
||||
var match = list.FirstOrDefault(p => p.Name == savedName)
|
||||
?? list.FirstOrDefault(p => p.IsDefault)
|
||||
?? (list.Count > 0 ? list[0] : null);
|
||||
|
||||
_suppressPrinterSave = true;
|
||||
SelectedPrinter = match;
|
||||
_suppressPrinterSave = false;
|
||||
|
||||
PrinterStatus = list.Count > 0 ? $"共 {list.Count} 台打印机" : "未检测到打印机";
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
Printers.Clear();
|
||||
_suppressPrinterSave = true;
|
||||
SelectedPrinter = null;
|
||||
_suppressPrinterSave = false;
|
||||
PrinterStatus = verbose
|
||||
? $"PrintDot 未连接:{ex.Message}"
|
||||
: "PrintDot 未连接";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowCached()
|
||||
{
|
||||
var cached = _printTemplateService.GetCached();
|
||||
if (cached.Count == 0) return;
|
||||
_allTemplates = cached.ToList();
|
||||
ApplyFilter();
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
private async Task RefreshSilentlyAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var list = await _printTemplateService.RefreshCacheAsync().ConfigureAwait(false);
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
_allTemplates = list.ToList();
|
||||
ApplyFilter();
|
||||
UpdateStatus();
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 静默失败:保留当前缓存内容不动,不显示错误
|
||||
var cached = _printTemplateService.GetCached();
|
||||
if (cached.Count > 0 && _allTemplates.Count == 0)
|
||||
{
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
_allTemplates = cached.ToList();
|
||||
ApplyFilter();
|
||||
UpdateStatus();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyFilter()
|
||||
{
|
||||
IEnumerable<PrintTemplate> result = _allTemplates;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(FilterCode))
|
||||
result = result.Where(t => (t.TemplateCode ?? string.Empty)
|
||||
.Contains(FilterCode, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(FilterName))
|
||||
result = result.Where(t => (t.TemplateName ?? string.Empty)
|
||||
.Contains(FilterName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(FilterCategory))
|
||||
result = result.Where(t => (t.Category ?? string.Empty)
|
||||
.Contains(FilterCategory, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var filtered = result.ToList();
|
||||
|
||||
// 原地差量更新,避免滚动位置重置和闪烁
|
||||
for (int i = Templates.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (!filtered.Any(t => t.Id == Templates[i].Id))
|
||||
Templates.RemoveAt(i);
|
||||
}
|
||||
for (int i = 0; i < filtered.Count; i++)
|
||||
{
|
||||
var item = filtered[i];
|
||||
var existingIdx = -1;
|
||||
for (int j = 0; j < Templates.Count; j++)
|
||||
{
|
||||
if (Templates[j].Id == item.Id) { existingIdx = j; break; }
|
||||
}
|
||||
if (existingIdx < 0)
|
||||
Templates.Insert(i, item);
|
||||
else
|
||||
{
|
||||
if (existingIdx != i) Templates.Move(existingIdx, i);
|
||||
Templates[i] = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateStatus()
|
||||
{
|
||||
var hasFilter = !string.IsNullOrWhiteSpace(FilterCode)
|
||||
|| !string.IsNullOrWhiteSpace(FilterName)
|
||||
|| !string.IsNullOrWhiteSpace(FilterCategory);
|
||||
StatusMessage = hasFilter
|
||||
? $"筛选结果 {Templates.Count} / {_allTemplates.Count} 个"
|
||||
: _allTemplates.Count > 0
|
||||
? $"共 {_allTemplates.Count} 个模板"
|
||||
: "暂无模板";
|
||||
}
|
||||
|
||||
private void ShowPreview(PrintTemplate template)
|
||||
{
|
||||
if (template == null) return;
|
||||
ShowPreviewAsync(template);
|
||||
}
|
||||
|
||||
private async Task ShowPreviewAsync(PrintTemplate template)
|
||||
{
|
||||
// 列表缓存可能不含 templateJson(大字段),按需通过 queryByCode 单独拉取
|
||||
var json = template.TemplateJson;
|
||||
if (string.IsNullOrWhiteSpace(json) || json == "{}")
|
||||
{
|
||||
try
|
||||
{
|
||||
var full = await _printTemplateService.GetByCodeAsync(template.TemplateCode ?? "");
|
||||
json = full?.TemplateJson;
|
||||
}
|
||||
catch { /* 保持 json 为 null,预览窗口显示"尚未设计" */ }
|
||||
}
|
||||
|
||||
var win = new PrintPreviewWindow(template, json, _printDotService, SelectedPrinter?.Name)
|
||||
{
|
||||
Owner = Application.Current.MainWindow
|
||||
};
|
||||
win.Show();
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,12 @@ using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Core.Entity;
|
||||
using YY.Admin.Core.Events;
|
||||
using YY.Admin.Core.Helper;
|
||||
using YY.Admin.Core.Entity;
|
||||
using YY.Admin.Core.Services;
|
||||
using YY.Admin.Services.Service;
|
||||
using YY.Admin.Views.Print;
|
||||
using YY.Admin.Views.RawMaterialCard;
|
||||
|
||||
namespace YY.Admin.ViewModels.RawMaterialCard;
|
||||
@@ -18,6 +19,7 @@ public class RawMaterialCardListViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IRawMaterialCardService _cardService;
|
||||
private readonly IJeecgDictSyncService _dictSyncService;
|
||||
private readonly IPrintDotService _printDotService;
|
||||
private SubscriptionToken? _changedToken;
|
||||
private SubscriptionToken? _syncConflictToken;
|
||||
|
||||
@@ -60,6 +62,7 @@ public class RawMaterialCardListViewModel : BaseViewModel
|
||||
public DelegateCommand AddCommand { get; }
|
||||
public DelegateCommand<MesXslRawMaterialCard> EditCommand { get; }
|
||||
public DelegateCommand<MesXslRawMaterialCard> DeleteCommand { get; }
|
||||
public DelegateCommand<MesXslRawMaterialCard> PrintCommand { get; }
|
||||
public DelegateCommand<MesXslRawMaterialCard> TogglePriorityCommand { get; }
|
||||
public DelegateCommand PrevPageCommand { get; }
|
||||
public DelegateCommand NextPageCommand { get; }
|
||||
@@ -67,11 +70,13 @@ public class RawMaterialCardListViewModel : BaseViewModel
|
||||
public RawMaterialCardListViewModel(
|
||||
IRawMaterialCardService cardService,
|
||||
IJeecgDictSyncService dictSyncService,
|
||||
IPrintDotService printDotService,
|
||||
IContainerExtension container,
|
||||
IRegionManager regionManager) : base(container, regionManager)
|
||||
{
|
||||
_cardService = cardService;
|
||||
_dictSyncService = dictSyncService;
|
||||
_printDotService = printDotService;
|
||||
|
||||
SearchCommand = new DelegateCommand(async () => { PageNo = 1; await LoadAsync(); });
|
||||
ResetCommand = new DelegateCommand(async () =>
|
||||
@@ -83,6 +88,7 @@ public class RawMaterialCardListViewModel : BaseViewModel
|
||||
AddCommand = new DelegateCommand(async () => await ShowAddDialogAsync());
|
||||
EditCommand = new DelegateCommand<MesXslRawMaterialCard>(async c => await ShowEditDialogAsync(c));
|
||||
DeleteCommand = new DelegateCommand<MesXslRawMaterialCard>(async c => await DeleteAsync(c));
|
||||
PrintCommand = new DelegateCommand<MesXslRawMaterialCard>(async c => await ShowPrintPreviewAsync(c));
|
||||
TogglePriorityCommand = new DelegateCommand<MesXslRawMaterialCard>(async c => await TogglePriorityAsync(c));
|
||||
PrevPageCommand = new DelegateCommand(async () => { if (PageNo > 1) { PageNo--; await LoadAsync(); } });
|
||||
NextPageCommand = new DelegateCommand(async () => { if ((long)PageNo * PageSize < Total) { PageNo++; await LoadAsync(); } });
|
||||
@@ -255,6 +261,65 @@ public class RawMaterialCardListViewModel : BaseViewModel
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowPrintPreviewAsync(MesXslRawMaterialCard card)
|
||||
{
|
||||
if (card?.Id == null) return;
|
||||
try
|
||||
{
|
||||
IsLoading = true;
|
||||
var (templateJson, printDataJson, errorMessage) = await _cardService.PrepareNativePrintAsync(card.Id);
|
||||
if (errorMessage != null)
|
||||
{
|
||||
Growl.Error(errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// 构造一个最简的 PrintTemplate 对象用于传入 PrintPreviewWindow(供显示标题 / 纸张信息)
|
||||
var tpl = BuildPrintTemplateFromJson(templateJson);
|
||||
|
||||
var win = new PrintPreviewWindow(tpl, templateJson, _printDotService, null, printDataJson)
|
||||
{
|
||||
Owner = Application.Current.MainWindow,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
||||
};
|
||||
win.Show();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Growl.Error($"打开打印预览失败:{ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static PrintTemplate BuildPrintTemplateFromJson(string templateJson)
|
||||
{
|
||||
try
|
||||
{
|
||||
var root = System.Text.Json.JsonDocument.Parse(templateJson).RootElement;
|
||||
double w = 210, h = 297;
|
||||
if (root.TryGetProperty("page", out var page))
|
||||
{
|
||||
if (page.TryGetProperty("width", out var wEl)) w = wEl.GetDouble();
|
||||
if (page.TryGetProperty("height", out var hEl)) h = hEl.GetDouble();
|
||||
}
|
||||
return new PrintTemplate
|
||||
{
|
||||
TemplateName = "原材料卡片",
|
||||
TemplateCode = "MES_RAW_MATERIAL_CARD",
|
||||
PaperWidthMm = w,
|
||||
PaperHeightMm = h,
|
||||
PaperOrientation = w > h ? "横向" : "纵向",
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new PrintTemplate { TemplateName = "原材料卡片", TemplateCode = "MES_RAW_MATERIAL_CARD" };
|
||||
}
|
||||
}
|
||||
|
||||
protected override void CleanUp()
|
||||
{
|
||||
base.CleanUp();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using HandyControl.Controls;
|
||||
using HandyControl.Data;
|
||||
using HandyControl.Tools.Extension;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
@@ -366,44 +367,62 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
AddSplitDetailCommand.RaiseCanExecuteChanged();
|
||||
}
|
||||
|
||||
/// <summary>校验并提交新增/编辑;不弹关闭、不执行独立页的 InitializeForAdd。成功时 Result=true。</summary>
|
||||
protected async Task<bool> PersistEntryCoreAsync()
|
||||
{
|
||||
if (Entry == null) return false;
|
||||
ApplySplitDetailsToEntry();
|
||||
ApplyDefaultEntryStatusForAdd();
|
||||
ApplyHiddenFieldDefaultsForAdd();
|
||||
|
||||
var missing = new List<string>();
|
||||
if (string.IsNullOrWhiteSpace(Entry.MaterialId)) missing.Add("密炼物料");
|
||||
if (string.IsNullOrWhiteSpace(Entry.BillNo)) missing.Add("榜单号");
|
||||
if (string.IsNullOrWhiteSpace(Entry.UnloadOperator)) missing.Add("卸货人");
|
||||
if (string.IsNullOrWhiteSpace(Entry.SupplierName)) missing.Add("供应商名称");
|
||||
if (missing.Count > 0)
|
||||
{
|
||||
HandyControl.Controls.MessageBox.Warning($"以下必填项不能为空:{string.Join("、", missing)}");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok;
|
||||
if (IsAddMode)
|
||||
{
|
||||
ok = await _entryService.AddAsync(Entry);
|
||||
if (!ok) { HandyControl.Controls.MessageBox.Error("新增失败!"); return false; }
|
||||
}
|
||||
else
|
||||
{
|
||||
ok = await _entryService.EditAsync(Entry);
|
||||
if (!ok) { HandyControl.Controls.MessageBox.Error("编辑失败!"); return false; }
|
||||
}
|
||||
Result = ok;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected virtual async Task SaveAsync()
|
||||
{
|
||||
if (Entry == null) return;
|
||||
try
|
||||
{
|
||||
ApplySplitDetailsToEntry();
|
||||
ApplyDefaultEntryStatusForAdd();
|
||||
ApplyHiddenFieldDefaultsForAdd();
|
||||
if (!await PersistEntryCoreAsync()) return;
|
||||
|
||||
var missing = new List<string>();
|
||||
if (string.IsNullOrWhiteSpace(Entry.MaterialId)) missing.Add("密炼物料");
|
||||
if (string.IsNullOrWhiteSpace(Entry.BillNo)) missing.Add("榜单号");
|
||||
if (string.IsNullOrWhiteSpace(Entry.UnloadOperator)) missing.Add("卸货人");
|
||||
if (string.IsNullOrWhiteSpace(Entry.SupplierName)) missing.Add("供应商名称");
|
||||
if (missing.Count > 0)
|
||||
{
|
||||
HandyControl.Controls.MessageBox.Warning($"以下必填项不能为空:{string.Join("、", missing)}");
|
||||
return;
|
||||
}
|
||||
|
||||
bool ok;
|
||||
if (IsAddMode)
|
||||
{
|
||||
ok = await _entryService.AddAsync(Entry);
|
||||
if (ok) HandyControl.Controls.MessageBox.Success("新增成功!");
|
||||
else { HandyControl.Controls.MessageBox.Error("新增失败!"); return; }
|
||||
}
|
||||
else
|
||||
{
|
||||
ok = await _entryService.EditAsync(Entry);
|
||||
if (!ok) { HandyControl.Controls.MessageBox.Error("编辑失败!"); return; }
|
||||
}
|
||||
Result = ok;
|
||||
if (IsAddMode && CloseAction == null)
|
||||
{
|
||||
// 独立新增页面:保存成功后自动清空表单,便于连续录入
|
||||
InitializeForAdd();
|
||||
return;
|
||||
// 非模态轻提示,约 1 秒后自动消失(与独立页「保存并打印」时的体验一致)
|
||||
Growl.Success(new GrowlInfo
|
||||
{
|
||||
Message = "保存成功",
|
||||
ShowDateTime = false,
|
||||
WaitTime = 1
|
||||
});
|
||||
if (CloseAction == null)
|
||||
{
|
||||
// 独立新增页面:保存成功后自动清空表单,便于连续录入
|
||||
InitializeForAdd();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
CloseAction?.Invoke();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -145,12 +145,34 @@
|
||||
|
||||
<Grid x:Name="MainContentGrid">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition
|
||||
x:Name="LeftMenuTreeCol"
|
||||
Width="250"
|
||||
MinWidth="50"
|
||||
MaxWidth="{Binding ActualWidth, ElementName=MainContentGrid, Converter={StaticResource MaxWidthConverter}, ConverterParameter=50}"/>
|
||||
<ColumnDefinition Width="4"/>
|
||||
<ColumnDefinition x:Name="LeftMenuTreeCol">
|
||||
<ColumnDefinition.Style>
|
||||
<Style TargetType="ColumnDefinition">
|
||||
<Setter Property="Width" Value="250"/>
|
||||
<Setter Property="MinWidth" Value="50"/>
|
||||
<Setter Property="MaxWidth" Value="{Binding ActualWidth, ElementName=MainContentGrid, Converter={StaticResource MaxWidthConverter}, ConverterParameter=50}"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsMenuTreePanelCollapsed}" Value="True">
|
||||
<Setter Property="Width" Value="0"/>
|
||||
<Setter Property="MinWidth" Value="0"/>
|
||||
<Setter Property="MaxWidth" Value="0"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</ColumnDefinition.Style>
|
||||
</ColumnDefinition>
|
||||
<ColumnDefinition>
|
||||
<ColumnDefinition.Style>
|
||||
<Style TargetType="ColumnDefinition">
|
||||
<Setter Property="Width" Value="4"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsMenuTreePanelCollapsed}" Value="True">
|
||||
<Setter Property="Width" Value="0"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</ColumnDefinition.Style>
|
||||
</ColumnDefinition>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<!-- Sidebar侧边栏区域 -->
|
||||
@@ -159,6 +181,16 @@
|
||||
BorderThickness="0,0,1,0"
|
||||
BorderBrush="{DynamicResource BorderBrush}"
|
||||
Background="{DynamicResource RegionBrush}">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsMenuTreePanelCollapsed}" Value="True">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<ContentControl prism:RegionManager.RegionName="{x:Static consts:CommonConst.MenuRegion}"/>
|
||||
</Border>
|
||||
<!-- 拖拽分隔条 -->
|
||||
@@ -170,7 +202,18 @@
|
||||
ShowsPreview="True"
|
||||
Cursor="SizeWE"
|
||||
Background="{DynamicResource RegionBrush}"
|
||||
PreviewStyle="{StaticResource GridSplitterPreviewStyle}"/>
|
||||
PreviewStyle="{StaticResource GridSplitterPreviewStyle}">
|
||||
<GridSplitter.Style>
|
||||
<Style TargetType="GridSplitter">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsMenuTreePanelCollapsed}" Value="True">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</GridSplitter.Style>
|
||||
</GridSplitter>
|
||||
<!-- 主内容区域 -->
|
||||
<Border
|
||||
Grid.Column="2"
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<Window x:Class="YY.Admin.Views.Print.PrintBizTemplateBindDetailWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
Title="业务打印绑定详情"
|
||||
Width="760" Height="580"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
ResizeMode="CanResize">
|
||||
<DockPanel Margin="16">
|
||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0 12 0 0">
|
||||
<TextBlock x:Name="TxtMeta" Foreground="#666" VerticalAlignment="Center" Margin="0 0 16 0"/>
|
||||
<Button Content="关闭" Width="88" Height="32" IsCancel="True" IsDefault="True" Click="OnCloseClick"/>
|
||||
</StackPanel>
|
||||
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel>
|
||||
<hc:Row Margin="0 0 0 8">
|
||||
<hc:Col Span="12">
|
||||
<hc:TextBox x:Name="TxtId" hc:InfoElement.Title="主键" hc:InfoElement.TitlePlacement="Left" hc:InfoElement.TitleWidth="80"
|
||||
IsReadOnly="True"/>
|
||||
</hc:Col>
|
||||
</hc:Row>
|
||||
<hc:Row Margin="0 0 0 8">
|
||||
<hc:Col Layout="{hc:ColLayout Xs=12, Md=6}">
|
||||
<hc:TextBox x:Name="TxtBizCode" hc:InfoElement.Title="业务编码" hc:InfoElement.TitlePlacement="Left" hc:InfoElement.TitleWidth="80"
|
||||
IsReadOnly="True"/>
|
||||
</hc:Col>
|
||||
<hc:Col Layout="{hc:ColLayout Xs=12, Md=6}">
|
||||
<hc:TextBox x:Name="TxtBizName" hc:InfoElement.Title="业务名称" hc:InfoElement.TitlePlacement="Left" hc:InfoElement.TitleWidth="80"
|
||||
IsReadOnly="True"/>
|
||||
</hc:Col>
|
||||
</hc:Row>
|
||||
<hc:Row Margin="0 0 0 8">
|
||||
<hc:Col Layout="{hc:ColLayout Xs=12, Md=6}">
|
||||
<hc:TextBox x:Name="TxtTemplateId" hc:InfoElement.Title="模板Id" hc:InfoElement.TitlePlacement="Left" hc:InfoElement.TitleWidth="80"
|
||||
IsReadOnly="True"/>
|
||||
</hc:Col>
|
||||
<hc:Col Layout="{hc:ColLayout Xs=12, Md=6}">
|
||||
<hc:TextBox x:Name="TxtTemplateCode" hc:InfoElement.Title="模板编码" hc:InfoElement.TitlePlacement="Left" hc:InfoElement.TitleWidth="80"
|
||||
IsReadOnly="True"/>
|
||||
</hc:Col>
|
||||
</hc:Row>
|
||||
<hc:Row Margin="0 0 0 12">
|
||||
<hc:Col Span="12">
|
||||
<hc:TextBox x:Name="TxtRemark" hc:InfoElement.Title="备注" hc:InfoElement.TitlePlacement="Left" hc:InfoElement.TitleWidth="80"
|
||||
IsReadOnly="True"/>
|
||||
</hc:Col>
|
||||
</hc:Row>
|
||||
|
||||
<GroupBox Header="字段映射 JSON">
|
||||
<TextBox x:Name="TxtMappingJson"
|
||||
MinHeight="260"
|
||||
AcceptsReturn="True"
|
||||
TextWrapping="Wrap"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
FontFamily="Consolas"
|
||||
FontSize="12"
|
||||
IsReadOnly="True"
|
||||
BorderThickness="0"/>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Text.Json;
|
||||
using System.Windows;
|
||||
using YY.Admin.Core.Entity;
|
||||
|
||||
namespace YY.Admin.Views.Print;
|
||||
|
||||
public partial class PrintBizTemplateBindDetailWindow
|
||||
{
|
||||
public PrintBizTemplateBindDetailWindow(PrintBizTemplateBind model)
|
||||
{
|
||||
InitializeComponent();
|
||||
TxtId.Text = model.Id ?? "";
|
||||
TxtBizCode.Text = model.BizCode ?? "";
|
||||
TxtBizName.Text = model.BizName ?? "";
|
||||
TxtTemplateId.Text = model.TemplateId ?? "";
|
||||
TxtTemplateCode.Text = model.TemplateCode ?? "";
|
||||
TxtRemark.Text = model.Remark ?? "";
|
||||
|
||||
var raw = model.FieldMappingJson ?? "";
|
||||
if (!string.IsNullOrWhiteSpace(raw))
|
||||
{
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(raw);
|
||||
TxtMappingJson.Text = JsonSerializer.Serialize(doc, new JsonSerializerOptions { WriteIndented = true });
|
||||
}
|
||||
catch
|
||||
{
|
||||
TxtMappingJson.Text = raw;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
TxtMappingJson.Text = "[]";
|
||||
}
|
||||
|
||||
var ct = model.CreateTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-";
|
||||
var ut = model.UpdateTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-";
|
||||
TxtMeta.Text = $"创建:{ct} 更新:{ut} 创建人:{model.CreateBy ?? "-"} 更新人:{model.UpdateBy ?? "-"}";
|
||||
}
|
||||
|
||||
private void OnCloseClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<UserControl x:Class="YY.Admin.Views.Print.PrintBizTemplateBindListView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid Style="{StaticResource BaseViewStyle}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Grid.Row="0" CornerRadius="4" Margin="0 0 -10 0">
|
||||
<hc:Row>
|
||||
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=6, Xl=4}">
|
||||
<hc:TextBox Text="{Binding FilterBizCode, UpdateSourceTrigger=PropertyChanged}"
|
||||
Margin="0 0 10 10"
|
||||
hc:InfoElement.Title="业务编码"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.TitleWidth="65"
|
||||
hc:InfoElement.Placeholder="菜单权限 id 或编码"
|
||||
hc:InfoElement.ShowClearButton="True"/>
|
||||
</hc:Col>
|
||||
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=6, Xl=4}">
|
||||
<hc:TextBox Text="{Binding FilterBizName, UpdateSourceTrigger=PropertyChanged}"
|
||||
Margin="0 0 10 10"
|
||||
hc:InfoElement.Title="业务名称"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.TitleWidth="65"
|
||||
hc:InfoElement.Placeholder="业务名称"
|
||||
hc:InfoElement.ShowClearButton="True"/>
|
||||
</hc:Col>
|
||||
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=6, Xl=4}">
|
||||
<hc:TextBox Text="{Binding FilterTemplateCode, UpdateSourceTrigger=PropertyChanged}"
|
||||
Margin="0 0 10 10"
|
||||
hc:InfoElement.Title="模板编码"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.TitleWidth="65"
|
||||
hc:InfoElement.Placeholder="打印模板编码"
|
||||
hc:InfoElement.ShowClearButton="True"/>
|
||||
</hc:Col>
|
||||
</hc:Row>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Row="1" Margin="0,10">
|
||||
<hc:UniformSpacingPanel Spacing="10" VerticalAlignment="Center">
|
||||
<Button Style="{StaticResource ButtonPrimary}" Command="{Binding SearchCommand}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<md:PackIcon Kind="Search"/>
|
||||
<TextBlock Text="搜索" Style="{StaticResource IconButtonStyle}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Style="{StaticResource ButtonDefault}" Command="{Binding ResetCommand}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<md:PackIcon Kind="Refresh"/>
|
||||
<TextBlock Text="重置" Style="{StaticResource IconButtonStyle}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</hc:UniformSpacingPanel>
|
||||
</Border>
|
||||
|
||||
<DataGrid Grid.Row="2"
|
||||
ItemsSource="{Binding Items}"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="True"
|
||||
CanUserAddRows="False"
|
||||
SelectionMode="Extended"
|
||||
SelectionUnit="FullRow"
|
||||
RowHeaderWidth="55"
|
||||
GridLinesVisibility="Horizontal"
|
||||
HorizontalGridLinesBrush="#FFEDEDED"
|
||||
VerticalGridLinesBrush="Transparent"
|
||||
HeadersVisibility="All"
|
||||
ColumnHeaderStyle="{StaticResource CusDataGridColumnHeaderStyle}"
|
||||
Style="{StaticResource CusDataGridStyle}"
|
||||
hc:DataGridAttach.ShowSelectAllButton="True"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Auto">
|
||||
<DataGrid.RowHeaderTemplate>
|
||||
<DataTemplate>
|
||||
<CheckBox IsChecked="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=DataGridRow}}"/>
|
||||
</DataTemplate>
|
||||
</DataGrid.RowHeaderTemplate>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="业务编码" Binding="{Binding BizCode}" Width="140" CellStyle="{StaticResource CusDataGridCellStyle}"/>
|
||||
<DataGridTextColumn Header="业务名称" Binding="{Binding BizName}" Width="*" MinWidth="120" CellStyle="{StaticResource CusDataGridCellStyle}"/>
|
||||
<DataGridTextColumn Header="模板编码" Binding="{Binding TemplateCode}" Width="140" CellStyle="{StaticResource CusDataGridCellStyle}"/>
|
||||
<DataGridTextColumn Header="备注" Binding="{Binding Remark}" Width="160" CellStyle="{StaticResource CusDataGridCellStyle}"/>
|
||||
<DataGridTextColumn Header="创建时间" Binding="{Binding CreateTime, StringFormat=yyyy-MM-dd HH:mm}" Width="130" CellStyle="{StaticResource CusDataGridCellStyle}"/>
|
||||
<DataGridTemplateColumn Header="操作" Width="88" CanUserSort="False" CanUserResize="False">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Button Content="详情"
|
||||
Command="{Binding DataContext.DetailCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
|
||||
CommandParameter="{Binding}"
|
||||
Style="{StaticResource ButtonPrimary}"
|
||||
Padding="0" Height="26" FontSize="12"
|
||||
Margin="4,0"/>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,0">
|
||||
<TextBlock Text="{Binding StatusMessage}"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace YY.Admin.Views.Print;
|
||||
|
||||
public partial class PrintBizTemplateBindListView
|
||||
{
|
||||
public PrintBizTemplateBindListView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
145
yy-admin-master/YY.Admin/Views/Print/PrintPreviewWindow.xaml
Normal file
145
yy-admin-master/YY.Admin/Views/Print/PrintPreviewWindow.xaml
Normal file
@@ -0,0 +1,145 @@
|
||||
<hc:Window x:Class="YY.Admin.Views.Print.PrintPreviewWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
|
||||
Width="1200" Height="760"
|
||||
MinWidth="900" MinHeight="560"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
ResizeMode="CanResize"
|
||||
Background="White"
|
||||
BorderBrush="#f0f0f0"
|
||||
BorderThickness="1"
|
||||
Title="打印预览">
|
||||
|
||||
<Window.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml"/>
|
||||
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="50"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 顶部工具栏 -->
|
||||
<Border Grid.Row="0"
|
||||
Background="#fafafa"
|
||||
BorderBrush="#f0f0f0"
|
||||
BorderThickness="0,0,0,1">
|
||||
<Grid Margin="16,0">
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<TextBlock x:Name="TbTemplateName"
|
||||
FontSize="14" FontWeight="SemiBold"
|
||||
Foreground="#333333"/>
|
||||
<TextBlock x:Name="TbTemplateCode"
|
||||
FontSize="11" Foreground="#888888" Margin="0,1,0,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock x:Name="TbStatus"
|
||||
FontSize="12" Foreground="#888888"
|
||||
VerticalAlignment="Center" Margin="0,0,12,0"
|
||||
MaxWidth="320"
|
||||
TextWrapping="NoWrap"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
ToolTipService.ShowDuration="30000"/>
|
||||
<TextBlock Text="打印机:" VerticalAlignment="Center"
|
||||
FontSize="12" Foreground="#333333"/>
|
||||
<ComboBox x:Name="PrinterCombo"
|
||||
MinWidth="200" Height="30"
|
||||
VerticalContentAlignment="Center"
|
||||
DisplayMemberPath="Name"/>
|
||||
<Button x:Name="BtnRefreshPrinters"
|
||||
Content="刷新打印机"
|
||||
Click="RefreshPrinters_Click"
|
||||
Margin="6,0,0,0" Height="30" Padding="10,0" FontSize="12"
|
||||
Style="{StaticResource ButtonDefault}"/>
|
||||
<Button x:Name="BtnPrint"
|
||||
Content="打印"
|
||||
Click="Print_Click"
|
||||
Margin="12,0,0,0" Width="84" Height="30" FontSize="13"
|
||||
Style="{StaticResource ButtonPrimary}"/>
|
||||
<Button Content="关闭" Click="CloseButton_Click"
|
||||
Margin="8,0,0,0" Width="72" Height="30" FontSize="13"
|
||||
Style="{StaticResource ButtonDefault}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="360"/>
|
||||
<ColumnDefinition Width="8"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 左侧参数 JSON 区 -->
|
||||
<Border Grid.Column="0"
|
||||
BorderBrush="#f0f0f0"
|
||||
BorderThickness="0,0,1,0"
|
||||
Background="#fafafa"
|
||||
Padding="10">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Text="参数JSON"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#333333"/>
|
||||
|
||||
<StackPanel Grid.Row="1"
|
||||
Orientation="Horizontal"
|
||||
Margin="0,8,0,8">
|
||||
<Button Content="根据画布生成"
|
||||
Click="GenerateMockJson_Click"
|
||||
Height="28"
|
||||
Padding="10,0"
|
||||
FontSize="12"
|
||||
Style="{StaticResource ButtonDefault}"/>
|
||||
<Button Content="重新渲染"
|
||||
Click="RenderByParamJson_Click"
|
||||
Margin="8,0,0,0"
|
||||
Height="28"
|
||||
Padding="10,0"
|
||||
FontSize="12"
|
||||
Style="{StaticResource ButtonPrimary}"/>
|
||||
</StackPanel>
|
||||
|
||||
<TextBox x:Name="TbParamJson"
|
||||
Grid.Row="2"
|
||||
AcceptsReturn="True"
|
||||
AcceptsTab="True"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
FontFamily="Consolas"
|
||||
FontSize="12"
|
||||
TextWrapping="NoWrap"
|
||||
BorderBrush="#d9d9d9"
|
||||
BorderThickness="1"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<GridSplitter Grid.Column="1"
|
||||
Width="8"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
ResizeBehavior="PreviousAndNext"
|
||||
ShowsPreview="True"
|
||||
Background="#f0f0f0"
|
||||
Cursor="SizeWE"/>
|
||||
|
||||
<!-- 右侧 WebView2 预览区 -->
|
||||
<wv2:WebView2 Grid.Column="2" x:Name="WebView" DefaultBackgroundColor="Transparent"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</hc:Window>
|
||||
535
yy-admin-master/YY.Admin/Views/Print/PrintPreviewWindow.xaml.cs
Normal file
535
yy-admin-master/YY.Admin/Views/Print/PrintPreviewWindow.xaml.cs
Normal file
@@ -0,0 +1,535 @@
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Windows;
|
||||
using YY.Admin.Core.Entity;
|
||||
using YY.Admin.Core.Services;
|
||||
using YY.Admin.Services.Service.Print;
|
||||
|
||||
namespace YY.Admin.Views.Print;
|
||||
|
||||
public partial class PrintPreviewWindow : HandyControl.Controls.Window
|
||||
{
|
||||
private readonly string _templateJson;
|
||||
private readonly PrintTemplate _template;
|
||||
private readonly IPrintDotService? _printDotService;
|
||||
private readonly string? _initialPrinterName;
|
||||
|
||||
public PrintPreviewWindow(PrintTemplate template, string? templateJson)
|
||||
: this(template, templateJson, null, null)
|
||||
{
|
||||
}
|
||||
|
||||
public PrintPreviewWindow(PrintTemplate template, string? templateJson,
|
||||
IPrintDotService? printDotService, string? selectedPrinterName,
|
||||
string? initialParamJson = null)
|
||||
{
|
||||
InitializeComponent();
|
||||
_template = template;
|
||||
_templateJson = templateJson ?? string.Empty;
|
||||
_printDotService = printDotService;
|
||||
_initialPrinterName = selectedPrinterName;
|
||||
|
||||
TbTemplateName.Text = template.TemplateName ?? "(未命名)";
|
||||
TbTemplateCode.Text = $"编码:{template.TemplateCode} " +
|
||||
$"尺寸:{template.PaperWidthMm ?? 210}×{template.PaperHeightMm ?? 297} mm " +
|
||||
$"方向:{template.PaperOrientation ?? "纵向"}";
|
||||
|
||||
TbParamJson.Text = !string.IsNullOrWhiteSpace(initialParamJson)
|
||||
? initialParamJson!
|
||||
: BuildMockParamJson(_templateJson);
|
||||
|
||||
// 没有 PrintDot 服务时禁用打印相关按钮
|
||||
if (_printDotService == null)
|
||||
{
|
||||
BtnPrint.IsEnabled = false;
|
||||
BtnRefreshPrinters.IsEnabled = false;
|
||||
PrinterCombo.IsEnabled = false;
|
||||
}
|
||||
|
||||
Loaded += async (_, _) =>
|
||||
{
|
||||
await LoadPreviewAsync();
|
||||
await LoadPrintersAsync(verbose: false);
|
||||
};
|
||||
}
|
||||
|
||||
private async Task LoadPreviewAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
SetStatus("加载中…");
|
||||
await WebView.EnsureCoreWebView2Async();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_templateJson) || _templateJson == "{}")
|
||||
{
|
||||
WebView.NavigateToString(BuildEmptyHtml());
|
||||
SetStatus("尚未设计模板内容");
|
||||
return;
|
||||
}
|
||||
|
||||
await RenderCurrentParamJsonAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SetStatus($"预览失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildEmptyHtml() => """
|
||||
<!DOCTYPE html>
|
||||
<html><head><meta charset="utf-8"/>
|
||||
<style>
|
||||
body { margin:0; background:#525659; display:flex;
|
||||
align-items:center; justify-content:center; height:100vh;
|
||||
font-family:"Microsoft YaHei",Arial,sans-serif; }
|
||||
.card { background:#fff; border-radius:8px; padding:48px 64px;
|
||||
text-align:center; box-shadow:0 6px 24px rgba(0,0,0,.4); }
|
||||
.icon { font-size:48px; color:#ccc; margin-bottom:16px; }
|
||||
.tip { font-size:15px; color:#888; }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="icon">📄</div>
|
||||
<div class="tip">该模板尚未设计内容</div>
|
||||
</div>
|
||||
</body></html>
|
||||
""";
|
||||
|
||||
/// <summary>
|
||||
/// 左侧参数 JSON 重新渲染预览(与后端预览一致:用参数 JSON 驱动模板绑定字段)。
|
||||
/// </summary>
|
||||
private async Task RenderCurrentParamJsonAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
SetStatus("渲染中…");
|
||||
|
||||
JsonObject dataObj;
|
||||
var text = TbParamJson.Text?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
dataObj = new JsonObject();
|
||||
}
|
||||
else
|
||||
{
|
||||
var node = JsonNode.Parse(text);
|
||||
if (node is not JsonObject obj)
|
||||
{
|
||||
WebView.NavigateToString(BuildErrorHtml("参数JSON必须是对象(JSON Object)"));
|
||||
SetStatus("参数JSON格式错误");
|
||||
return;
|
||||
}
|
||||
dataObj = obj;
|
||||
}
|
||||
|
||||
var html = NativePrintRenderService.RenderToHtml(_templateJson, dataObj);
|
||||
WebView.NavigateToString(html);
|
||||
SetStatus(string.Empty);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WebView.NavigateToString(BuildErrorHtml(ex.Message));
|
||||
SetStatus($"渲染失败:{ex.Message}");
|
||||
}
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据模板绑定字段生成参数 JSON(便于用户直接编辑并预览)。
|
||||
/// 与 web 端 nativeMockData.ts 保持一致:识别 mergeColumnKeys 让同组相邻行字段值相同,
|
||||
/// 以便在预览中触发 rowSpan 合并显示。
|
||||
/// </summary>
|
||||
private static string BuildMockParamJson(string templateJson)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(templateJson) || templateJson == "{}")
|
||||
return "{}";
|
||||
|
||||
try
|
||||
{
|
||||
var root = JsonNode.Parse(templateJson);
|
||||
var obj = new JsonObject();
|
||||
var elements = root?["elements"]?.AsArray() ?? new JsonArray();
|
||||
var fields = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var rng = new Random();
|
||||
|
||||
foreach (var el in elements.OfType<JsonObject>())
|
||||
{
|
||||
var type = (el["type"]?.ToString() ?? string.Empty).Trim();
|
||||
if (type is "table" or "detailTable")
|
||||
{
|
||||
var source = (el["source"]?.ToString() ?? "mainTable").Trim();
|
||||
if (!obj.ContainsKey(source))
|
||||
{
|
||||
var columns = el["columns"]?.AsArray() ?? new JsonArray();
|
||||
var colList = columns.OfType<JsonObject>().ToList();
|
||||
|
||||
// 解析合并字段顺序:根据 mergeColumnKeys 按 column.key 映射到 bindField
|
||||
var mergeKeys = (el["mergeColumnKeys"]?.AsArray() ?? new JsonArray())
|
||||
.Select(n => n?.ToString() ?? string.Empty)
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.ToList();
|
||||
var strictGrouping = !string.Equals(
|
||||
el["strictGrouping"]?.ToString() ?? "true", "false",
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var mergeFieldOrder = mergeKeys
|
||||
.Select(k => colList.FirstOrDefault(c => string.Equals(c["key"]?.ToString() ?? string.Empty, k, StringComparison.Ordinal)))
|
||||
.Where(c => c != null)
|
||||
.Select(c => (c!["bindField"]?.ToString() ?? c!["field"]?.ToString() ?? string.Empty).Trim())
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.ToList();
|
||||
|
||||
var rows = new JsonArray();
|
||||
JsonObject? prevRow = null;
|
||||
for (var i = 0; i < 8; i++)
|
||||
{
|
||||
var row = new JsonObject();
|
||||
foreach (var col in colList)
|
||||
{
|
||||
var field = (col["bindField"]?.ToString() ?? col["field"]?.ToString() ?? string.Empty).Trim();
|
||||
if (string.IsNullOrWhiteSpace(field)) continue;
|
||||
var contentType = (col["contentType"]?.ToString() ?? "text").Trim().ToLowerInvariant();
|
||||
fields.Add(field);
|
||||
|
||||
var mergeIndex = mergeFieldOrder.IndexOf(field);
|
||||
var enableMerge = mergeIndex >= 0;
|
||||
|
||||
if (enableMerge)
|
||||
{
|
||||
// 父级合并字段在 strictGrouping 下必须与前一行一致,才允许沿用前一行值
|
||||
var canFollowPrev = !strictGrouping || mergeIndex == 0 || (prevRow != null &&
|
||||
mergeFieldOrder.Take(mergeIndex).All(parent =>
|
||||
(prevRow[parent]?.ToString() ?? string.Empty) == (row[parent]?.ToString() ?? string.Empty)));
|
||||
|
||||
if (i > 0 && prevRow != null && canFollowPrev && rng.NextDouble() < 0.5)
|
||||
{
|
||||
// 沿用前一行此字段值,从而触发合并
|
||||
var prevVal = prevRow[field];
|
||||
row[field] = prevVal != null ? JsonNode.Parse(prevVal.ToJsonString()) : (JsonNode?)$"{field}_合并组1";
|
||||
}
|
||||
else
|
||||
{
|
||||
// 新合并组:随机 0-3 表示组编号
|
||||
row[field] = $"{field}_合并组{rng.Next(1, 5)}";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
row[field] = contentType switch
|
||||
{
|
||||
"number" => (i + 1) * 123.45,
|
||||
"amount" => (i + 1) * 24567.89,
|
||||
"qrcode" => BuildQrcodeMockValue(field, rng),
|
||||
"barcode" => BuildBarcodeMockValue(field, rng),
|
||||
"image" => $"https://picsum.photos/seed/{Uri.EscapeDataString(field + "_" + (i + 1))}/260/120",
|
||||
_ => $"{field}_示例值_{i + 1}"
|
||||
};
|
||||
}
|
||||
rows.Add(row);
|
||||
prevRow = row;
|
||||
}
|
||||
obj[source] = rows;
|
||||
}
|
||||
}
|
||||
|
||||
var bind = (el["bindField"]?.ToString() ?? string.Empty).Trim();
|
||||
if (!string.IsNullOrWhiteSpace(bind))
|
||||
fields.Add(bind);
|
||||
|
||||
// 针对单一元素按 type 提前预填合法 mock 值,避免下面兜底成"字段_示例值"
|
||||
// (barcode 含中文会导致 Code128 编码失败)。与 web 端 nativeMockData.ts 行为对齐。
|
||||
if (!string.IsNullOrWhiteSpace(bind) && !obj.ContainsKey(bind))
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case "barcode":
|
||||
obj[bind] = BuildBarcodeMockValue(bind, rng);
|
||||
break;
|
||||
case "qrcode":
|
||||
obj[bind] = BuildQrcodeMockValue(bind, rng);
|
||||
break;
|
||||
case "image":
|
||||
obj[bind] = $"https://picsum.photos/seed/{Uri.EscapeDataString(bind)}/260/120";
|
||||
break;
|
||||
case "date":
|
||||
obj[bind] = "2026-01-01";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// freeTable / 其它含 cells 的元素:根据 cell.contentType 预填合法 mock 值
|
||||
CollectCellsMock(el["cells"], obj, fields, rng);
|
||||
|
||||
// 提取 text 中的 {{field}} 占位符(支持内嵌)
|
||||
var text = el["text"]?.ToString() ?? string.Empty;
|
||||
foreach (Match m in Regex.Matches(text, @"\{\{\s*([\w\.]+)\s*\}\}"))
|
||||
{
|
||||
var key = m.Groups[1].Value.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(key))
|
||||
fields.Add(key);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var f in fields.OrderBy(x => x, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
if (f.Equals("pageNo", StringComparison.OrdinalIgnoreCase) || f.Equals("totalPages", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
if (!obj.ContainsKey(f))
|
||||
obj[f] = $"{f}_示例值";
|
||||
}
|
||||
|
||||
return obj.ToJsonString(new JsonSerializerOptions { WriteIndented = true });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "{}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成一个 ASCII 安全、Code128 可编码的条码 mock 值:BAR + 12 位数字 + 字段名前 6 位转大写字母。
|
||||
/// 与 web 端 nativeMockData.ts::buildBarcodeValue 同款规则。
|
||||
/// </summary>
|
||||
private static string BuildBarcodeMockValue(string field, Random rng)
|
||||
{
|
||||
var digits = rng.NextInt64(100000000000L, 999999999999L).ToString(CultureInfo.InvariantCulture);
|
||||
var suffix = new string((field ?? string.Empty)
|
||||
.Where(c => (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'))
|
||||
.Take(6).ToArray()).ToUpperInvariant();
|
||||
if (string.IsNullOrEmpty(suffix)) suffix = "BARCOD";
|
||||
return $"BAR{digits}{suffix}";
|
||||
}
|
||||
|
||||
/// <summary>生成 QR mock 值:纯 ASCII,便于 QRCoder 编码与人眼区分字段。</summary>
|
||||
private static string BuildQrcodeMockValue(string field, Random rng)
|
||||
{
|
||||
var safe = new string((field ?? string.Empty)
|
||||
.Where(c => (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_')
|
||||
.ToArray());
|
||||
if (string.IsNullOrEmpty(safe)) safe = "QR";
|
||||
return $"QR_{safe}_{rng.Next(100000, 999999)}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归扫描 cells 节点(freeTable、嵌套结构),按每个 cell 的 contentType 提前预填合法 mock 值。
|
||||
/// 不识别的 cell 继续走原来的 fields 兜底流程。
|
||||
/// </summary>
|
||||
private static void CollectCellsMock(JsonNode? node, JsonObject obj, ISet<string> fields, Random rng)
|
||||
{
|
||||
if (node == null) return;
|
||||
if (node is JsonObject o)
|
||||
{
|
||||
var bind = (o["bindField"]?.ToString() ?? string.Empty).Trim();
|
||||
if (!string.IsNullOrWhiteSpace(bind))
|
||||
{
|
||||
fields.Add(bind);
|
||||
if (!obj.ContainsKey(bind))
|
||||
{
|
||||
var ct = (o["contentType"]?.ToString() ?? "text").Trim().ToLowerInvariant();
|
||||
switch (ct)
|
||||
{
|
||||
case "barcode":
|
||||
obj[bind] = BuildBarcodeMockValue(bind, rng);
|
||||
break;
|
||||
case "qrcode":
|
||||
obj[bind] = BuildQrcodeMockValue(bind, rng);
|
||||
break;
|
||||
case "image":
|
||||
obj[bind] = $"https://picsum.photos/seed/{Uri.EscapeDataString(bind)}/260/120";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (var kv in o)
|
||||
CollectCellsMock(kv.Value, obj, fields, rng);
|
||||
return;
|
||||
}
|
||||
if (node is JsonArray arr)
|
||||
{
|
||||
foreach (var it in arr)
|
||||
CollectCellsMock(it, obj, fields, rng);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CollectBindFields(JsonNode? node, ISet<string> fields)
|
||||
{
|
||||
if (node == null) return;
|
||||
if (node is JsonObject o)
|
||||
{
|
||||
if (o.TryGetPropertyValue("bindField", out var bindNode))
|
||||
{
|
||||
var bind = bindNode?.ToString()?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(bind))
|
||||
fields.Add(bind);
|
||||
}
|
||||
foreach (var kv in o)
|
||||
CollectBindFields(kv.Value, fields);
|
||||
return;
|
||||
}
|
||||
if (node is JsonArray arr)
|
||||
{
|
||||
foreach (var it in arr)
|
||||
CollectBindFields(it, fields);
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildErrorHtml(string message)
|
||||
{
|
||||
var esc = message.Replace("&", "&").Replace("<", "<").Replace(">", ">");
|
||||
return "<html><head><meta charset=\"utf-8\"/><style>"
|
||||
+ "body{margin:0;background:#525659;display:flex;align-items:center;"
|
||||
+ "justify-content:center;height:100vh;font-family:'Microsoft YaHei',Arial,sans-serif;}"
|
||||
+ ".card{background:#fff;border-radius:8px;padding:32px 48px;text-align:center;"
|
||||
+ "box-shadow:0 6px 24px rgba(0,0,0,.4);max-width:560px;}"
|
||||
+ "</style></head><body><div class=\"card\">"
|
||||
+ "<div style=\"font-size:40px;margin-bottom:12px\">⚠️</div>"
|
||||
+ "<div style=\"font-size:13px;color:#e74c3c;word-break:break-all;\">渲染失败:" + esc + "</div>"
|
||||
+ "</div></body></html>";
|
||||
}
|
||||
|
||||
private async void RenderByParamJson_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await RenderCurrentParamJsonAsync();
|
||||
}
|
||||
|
||||
private async void GenerateMockJson_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
TbParamJson.Text = BuildMockParamJson(_templateJson);
|
||||
await RenderCurrentParamJsonAsync();
|
||||
}
|
||||
|
||||
private void CloseButton_Click(object sender, RoutedEventArgs e) => Close();
|
||||
|
||||
/// <summary>
|
||||
/// 设置顶部状态栏文字:单行显示首行概要,完整多行内容通过 ToolTip 兜底,避免把工具栏撑高。
|
||||
/// </summary>
|
||||
private void SetStatus(string? message)
|
||||
{
|
||||
var full = (message ?? string.Empty).Trim();
|
||||
var firstLine = full.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault() ?? string.Empty;
|
||||
TbStatus.Text = firstLine;
|
||||
TbStatus.ToolTip = full.Contains('\n') || full.Length > firstLine.Length ? full : null;
|
||||
}
|
||||
|
||||
// ── 打印机列表加载(PrintDot 桥接器) ────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 通过 PrintDot 桥接器拉取打印机列表,填充顶部下拉框,并按上次选择/系统默认/首台优先选中。
|
||||
/// </summary>
|
||||
private async Task LoadPrintersAsync(bool verbose)
|
||||
{
|
||||
if (_printDotService == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
if (verbose) SetStatus("刷新打印机中...");
|
||||
var list = await _printDotService.GetPrintersAsync();
|
||||
PrinterCombo.ItemsSource = list;
|
||||
|
||||
var preferName = _initialPrinterName
|
||||
?? PrintDotSettings.Load().SelectedPrinter;
|
||||
var match = list.FirstOrDefault(p => p.Name == preferName)
|
||||
?? list.FirstOrDefault(p => p.IsDefault)
|
||||
?? list.FirstOrDefault();
|
||||
PrinterCombo.SelectedItem = match;
|
||||
|
||||
SetStatus(list.Count > 0
|
||||
? $"已发现 {list.Count} 台打印机"
|
||||
: "未检测到打印机");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PrinterCombo.ItemsSource = null;
|
||||
SetStatus($"PrintDot 未连接:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async void RefreshPrinters_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await LoadPrintersAsync(verbose: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打印流程:
|
||||
/// 1) 用 WebView2.PrintToPdfAsync 按模板纸张尺寸生成 PDF(与 @page 一致,零边距);
|
||||
/// 2) 读取 PDF → Base64;
|
||||
/// 3) 通过 PrintDot 桥接器发送至本地物理打印机。
|
||||
/// 与后端 web 端 printNativeSchemaViaPrintDot 的行为基本一致,只是 HTML→PDF 改用 WebView2 内置能力,
|
||||
/// 比 html2canvas + jsPDF 更接近浏览器原生打印效果。
|
||||
/// </summary>
|
||||
private async void Print_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_printDotService == null)
|
||||
{
|
||||
HandyControl.Controls.Growl.Warning("PrintDot 服务不可用");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_templateJson) || _templateJson == "{}")
|
||||
{
|
||||
HandyControl.Controls.Growl.Warning("当前模板没有可打印内容");
|
||||
return;
|
||||
}
|
||||
|
||||
var selected = PrinterCombo.SelectedItem as PrintDotPrinter;
|
||||
if (selected == null || string.IsNullOrWhiteSpace(selected.Name))
|
||||
{
|
||||
HandyControl.Controls.Growl.Warning("请先选择打印机");
|
||||
return;
|
||||
}
|
||||
|
||||
BtnPrint.IsEnabled = false;
|
||||
var pdfPath = Path.Combine(Path.GetTempPath(), $"qhmes_print_{Guid.NewGuid():N}.pdf");
|
||||
try
|
||||
{
|
||||
// 1) 生成 PDF
|
||||
SetStatus("正在生成 PDF...");
|
||||
await WebView.EnsureCoreWebView2Async();
|
||||
|
||||
var pageW = (_template.PaperWidthMm ?? 210);
|
||||
var pageH = (_template.PaperHeightMm ?? 297);
|
||||
var settings = WebView.CoreWebView2.Environment.CreatePrintSettings();
|
||||
settings.PageWidth = pageW / 25.4d; // 毫米转英寸
|
||||
settings.PageHeight = pageH / 25.4d;
|
||||
settings.MarginTop = 0;
|
||||
settings.MarginBottom = 0;
|
||||
settings.MarginLeft = 0;
|
||||
settings.MarginRight = 0;
|
||||
settings.ShouldPrintBackgrounds = true;
|
||||
settings.ShouldPrintHeaderAndFooter = false;
|
||||
settings.Orientation = string.Equals(_template.PaperOrientation, "横向", StringComparison.Ordinal)
|
||||
? Microsoft.Web.WebView2.Core.CoreWebView2PrintOrientation.Landscape
|
||||
: Microsoft.Web.WebView2.Core.CoreWebView2PrintOrientation.Portrait;
|
||||
|
||||
var ok = await WebView.CoreWebView2.PrintToPdfAsync(pdfPath, settings);
|
||||
if (!ok || !File.Exists(pdfPath))
|
||||
throw new InvalidOperationException("生成 PDF 失败,请确认预览已加载完成");
|
||||
|
||||
// 2) 读取为 Base64
|
||||
var pdfBytes = await File.ReadAllBytesAsync(pdfPath);
|
||||
var pdfBase64 = Convert.ToBase64String(pdfBytes);
|
||||
|
||||
// 3) 通过 PrintDot 桥接器发送
|
||||
SetStatus($"正在通过 PrintDot 发送到「{selected.Name}」...");
|
||||
var jobName = string.IsNullOrWhiteSpace(_template.TemplateName) ? "QH-MES" : _template.TemplateName!;
|
||||
await _printDotService.PrintAsync(selected.Name, pdfBase64, jobName, copies: 1);
|
||||
|
||||
SetStatus("打印任务已发送");
|
||||
HandyControl.Controls.Growl.Success("打印任务已发送至 PrintDot");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 顶部状态栏单行显示首行,完整多行处理步骤在 Growl 弹窗中展示
|
||||
SetStatus($"打印失败:{ex.Message}");
|
||||
HandyControl.Controls.Growl.Error($"打印失败:{ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { if (File.Exists(pdfPath)) File.Delete(pdfPath); } catch { /* 忽略清理失败 */ }
|
||||
BtnPrint.IsEnabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
96
yy-admin-master/YY.Admin/Views/Print/PrintSettingsView.xaml
Normal file
96
yy-admin-master/YY.Admin/Views/Print/PrintSettingsView.xaml
Normal file
@@ -0,0 +1,96 @@
|
||||
<UserControl x:Class="YY.Admin.Views.Print.PrintSettingsView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Margin="24" MaxWidth="680">
|
||||
|
||||
<!-- 标题 -->
|
||||
<TextBlock Text="打印设置" FontSize="18" FontWeight="Bold" Margin="0,0,0,20"/>
|
||||
|
||||
<!-- PrintDot 连接配置 -->
|
||||
<Border BorderThickness="1" BorderBrush="{DynamicResource BorderBrush}" CornerRadius="4" Padding="16" Margin="0,0,0,16">
|
||||
<StackPanel>
|
||||
<TextBlock Text="PrintDot 桥接器连接" FontSize="14" FontWeight="SemiBold" Margin="0,0,0,12"/>
|
||||
|
||||
<Grid Margin="0,0,0,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="120"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="WebSocket 地址" VerticalAlignment="Center" FontSize="13"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding WsUrl, UpdateSourceTrigger=PropertyChanged}"
|
||||
hc:InfoElement.Placeholder="ws://192.168.x.x:1122/ws"
|
||||
FontSize="13" Padding="6,4"/>
|
||||
</Grid>
|
||||
|
||||
<TextBlock Text="格式:ws://<IP>:1122/ws,支持局域网任意 IP" FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}" Margin="120,0,0,12"/>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
|
||||
<Button Content="测试连接并获取打印机" Command="{Binding TestConnectionCommand}"
|
||||
Style="{StaticResource ButtonPrimary}" Padding="12,6" FontSize="13" Margin="0,0,8,0"/>
|
||||
<Button Content="保存设置" Command="{Binding SaveCommand}"
|
||||
Style="{StaticResource ButtonDefault}" Padding="12,6" FontSize="13"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 状态提示 -->
|
||||
<TextBlock Text="{Binding StatusMessage}" FontSize="12" Foreground="{DynamicResource InfoBrush}"
|
||||
Visibility="{Binding StatusMessage, Converter={StaticResource String2VisibilityConverter}}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 打印机列表 -->
|
||||
<Border BorderThickness="1" BorderBrush="{DynamicResource BorderBrush}" CornerRadius="4" Padding="16" Margin="0,0,0,16">
|
||||
<StackPanel>
|
||||
<TextBlock Text="可用打印机" FontSize="14" FontWeight="SemiBold" Margin="0,0,0,12"/>
|
||||
|
||||
<ListBox ItemsSource="{Binding Printers}" SelectedItem="{Binding SelectedPrinter}"
|
||||
MaxHeight="180" ScrollViewer.VerticalScrollBarVisibility="Auto">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal" Margin="4,2">
|
||||
<TextBlock Text="{Binding Name}" FontSize="13"/>
|
||||
<TextBlock Text=" (默认)" FontSize="11" Foreground="{DynamicResource InfoBrush}"
|
||||
Visibility="{Binding IsDefault, Converter={StaticResource Boolean2VisibilityConverter}}"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<TextBlock Margin="0,8,0,0" FontSize="12" Foreground="{DynamicResource SecondaryTextBrush}">
|
||||
已选打印机:<Run Text="{Binding SelectedPrinter.Name, FallbackValue='(未选择)'}"/>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 打印模板列表 -->
|
||||
<Border BorderThickness="1" BorderBrush="{DynamicResource BorderBrush}" CornerRadius="4" Padding="16">
|
||||
<StackPanel>
|
||||
<DockPanel Margin="0,0,0,12">
|
||||
<Button DockPanel.Dock="Right" Content="刷新模板" Command="{Binding RefreshTemplatesCommand}"
|
||||
Style="{StaticResource ButtonDefault}" Padding="10,4" FontSize="12"/>
|
||||
<TextBlock Text="打印模板" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center"/>
|
||||
</DockPanel>
|
||||
|
||||
<DataGrid ItemsSource="{Binding Templates}" AutoGenerateColumns="False"
|
||||
IsReadOnly="True" MaxHeight="240" GridLinesVisibility="Horizontal"
|
||||
HeadersVisibility="Column" FontSize="12">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="模板编码" Binding="{Binding TemplateCode}" Width="160"/>
|
||||
<DataGridTextColumn Header="模板名称" Binding="{Binding TemplateName}" Width="*"/>
|
||||
<DataGridTextColumn Header="分类" Binding="{Binding Category}" Width="80"/>
|
||||
<DataGridTextColumn Header="纸宽(mm)" Binding="{Binding PaperWidthMm}" Width="70"/>
|
||||
<DataGridTextColumn Header="纸高(mm)" Binding="{Binding PaperHeightMm}" Width="70"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 忙碌指示 -->
|
||||
<hc:LoadingCircle Visibility="{Binding IsBusy, Converter={StaticResource Boolean2VisibilityConverter}}"
|
||||
HorizontalAlignment="Center" Margin="0,16,0,0"/>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace YY.Admin.Views.Print;
|
||||
|
||||
public partial class PrintSettingsView : UserControl
|
||||
{
|
||||
public PrintSettingsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
159
yy-admin-master/YY.Admin/Views/Print/PrintTemplateListView.xaml
Normal file
159
yy-admin-master/YY.Admin/Views/Print/PrintTemplateListView.xaml
Normal file
@@ -0,0 +1,159 @@
|
||||
<UserControl x:Class="YY.Admin.Views.Print.PrintTemplateListView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid Style="{StaticResource BaseViewStyle}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 搜索条件区域 -->
|
||||
<Border Grid.Row="0" CornerRadius="4" Margin="0 0 -10 0">
|
||||
<hc:Row>
|
||||
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=6, Xl=4}">
|
||||
<hc:TextBox Text="{Binding FilterCode, UpdateSourceTrigger=PropertyChanged}"
|
||||
Margin="0 0 10 10"
|
||||
hc:InfoElement.Title="模板编码"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.TitleWidth="65"
|
||||
hc:InfoElement.Placeholder="请输入模板编码"
|
||||
hc:InfoElement.ShowClearButton="True"/>
|
||||
</hc:Col>
|
||||
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=6, Xl=4}">
|
||||
<hc:TextBox Text="{Binding FilterName, UpdateSourceTrigger=PropertyChanged}"
|
||||
Margin="0 0 10 10"
|
||||
hc:InfoElement.Title="模板名称"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.TitleWidth="65"
|
||||
hc:InfoElement.Placeholder="请输入模板名称"
|
||||
hc:InfoElement.ShowClearButton="True"/>
|
||||
</hc:Col>
|
||||
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=6, Xl=4}">
|
||||
<hc:TextBox Text="{Binding FilterCategory, UpdateSourceTrigger=PropertyChanged}"
|
||||
Margin="0 0 10 10"
|
||||
hc:InfoElement.Title="分类"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.TitleWidth="65"
|
||||
hc:InfoElement.Placeholder="请输入分类"
|
||||
hc:InfoElement.ShowClearButton="True"/>
|
||||
</hc:Col>
|
||||
</hc:Row>
|
||||
</Border>
|
||||
|
||||
<!-- 操作工具栏 -->
|
||||
<Border Grid.Row="1" Margin="0,10">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<hc:UniformSpacingPanel Grid.Column="0" Spacing="10" VerticalAlignment="Center">
|
||||
<Button Style="{StaticResource ButtonPrimary}" Command="{Binding SearchCommand}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<md:PackIcon Kind="Search"/>
|
||||
<TextBlock Text="搜索" Style="{StaticResource IconButtonStyle}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Style="{StaticResource ButtonDefault}" Command="{Binding ResetCommand}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<md:PackIcon Kind="Refresh"/>
|
||||
<TextBlock Text="重置" Style="{StaticResource IconButtonStyle}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</hc:UniformSpacingPanel>
|
||||
|
||||
<!-- PrintDot 打印机选择(与后端列表页对齐) -->
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="打印机:" VerticalAlignment="Center"
|
||||
FontSize="13" Foreground="#333333"/>
|
||||
<ComboBox ItemsSource="{Binding Printers}"
|
||||
SelectedItem="{Binding SelectedPrinter}"
|
||||
DisplayMemberPath="Name"
|
||||
MinWidth="220" Height="30"
|
||||
VerticalContentAlignment="Center"
|
||||
hc:InfoElement.Placeholder="请选择打印机"/>
|
||||
<Button Command="{Binding RefreshPrintersCommand}"
|
||||
Height="30" Padding="10,0" Margin="8,0,0,0"
|
||||
FontSize="12"
|
||||
Style="{StaticResource ButtonDefault}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<md:PackIcon Kind="Refresh" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="刷新打印机" Margin="4,0,0,0" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<TextBlock Text="{Binding PrinterStatus}"
|
||||
Margin="12,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<DataGrid Grid.Row="2"
|
||||
ItemsSource="{Binding Templates}"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="True"
|
||||
CanUserAddRows="False"
|
||||
SelectionMode="Extended"
|
||||
SelectionUnit="FullRow"
|
||||
RowHeaderWidth="55"
|
||||
GridLinesVisibility="Horizontal"
|
||||
HorizontalGridLinesBrush="#FFEDEDED"
|
||||
VerticalGridLinesBrush="Transparent"
|
||||
HeadersVisibility="All"
|
||||
ColumnHeaderStyle="{StaticResource CusDataGridColumnHeaderStyle}"
|
||||
Style="{StaticResource CusDataGridStyle}"
|
||||
hc:DataGridAttach.ShowSelectAllButton="True"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Auto">
|
||||
<DataGrid.RowHeaderTemplate>
|
||||
<DataTemplate>
|
||||
<CheckBox IsChecked="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=DataGridRow}}"/>
|
||||
</DataTemplate>
|
||||
</DataGrid.RowHeaderTemplate>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="模板编码" Binding="{Binding TemplateCode}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="160"/>
|
||||
<DataGridTextColumn Header="模板名称" Binding="{Binding TemplateName}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="*"/>
|
||||
<DataGridTextColumn Header="分类" Binding="{Binding Category}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="100"/>
|
||||
<DataGridTextColumn Header="纸宽(mm)" Binding="{Binding PaperWidthMm}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="90"/>
|
||||
<DataGridTextColumn Header="纸高(mm)" Binding="{Binding PaperHeightMm}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="90"/>
|
||||
<DataGridTextColumn Header="方向" Binding="{Binding PaperOrientation}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="80"/>
|
||||
<DataGridTextColumn Header="备注" Binding="{Binding Remark}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="180"/>
|
||||
<DataGridTextColumn Header="创建时间" Binding="{Binding CreateTime, StringFormat=yyyy-MM-dd HH:mm}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="130"/>
|
||||
<DataGridTemplateColumn Header="操作" Width="72" CanUserSort="False" CanUserResize="False">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Button Content="预览"
|
||||
Command="{Binding DataContext.PreviewCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
|
||||
CommandParameter="{Binding}"
|
||||
Style="{StaticResource ButtonPrimary}"
|
||||
Padding="0" Height="26" FontSize="12"
|
||||
Margin="4,0"/>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<!-- 底部状态栏 -->
|
||||
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,0">
|
||||
<TextBlock Text="{Binding StatusMessage}"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace YY.Admin.Views.Print;
|
||||
|
||||
public partial class PrintTemplateListView : UserControl
|
||||
{
|
||||
public PrintTemplateListView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -143,7 +143,7 @@
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTemplateColumn Header="操作" Width="160">
|
||||
<DataGridTemplateColumn Header="操作" Width="230">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<hc:UniformSpacingPanel Spacing="6" HorizontalAlignment="Center">
|
||||
@@ -152,6 +152,11 @@
|
||||
FontSize="12" Height="26" Padding="8,0"
|
||||
Command="{Binding DataContext.EditCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
|
||||
CommandParameter="{Binding}"/>
|
||||
<Button Content="打印"
|
||||
Style="{StaticResource ButtonInfo}"
|
||||
FontSize="12" Height="26" Padding="8,0"
|
||||
Command="{Binding DataContext.PrintCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
|
||||
CommandParameter="{Binding}"/>
|
||||
<Button Content="删除"
|
||||
Style="{StaticResource ButtonDanger}"
|
||||
FontSize="12" Height="26" Padding="8,0"
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
<hc:Window x:Class="YY.Admin.Views.RawMaterialEntry.RawMaterialCardGenerateConfirmWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
|
||||
Width="1416"
|
||||
Height="864"
|
||||
MinWidth="1176"
|
||||
MinHeight="744"
|
||||
Title="生成并打印原材料卡片"
|
||||
ResizeMode="CanResize">
|
||||
<Grid Margin="16">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Grid.Row="0" Margin="0,0,0,10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Text="即将生成的原材料卡片"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"/>
|
||||
<TextBlock Text="{Binding HeaderText}"
|
||||
Margin="0,4,0,0"
|
||||
FontSize="12"
|
||||
Foreground="#8C8C8C"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1"
|
||||
Orientation="Horizontal"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="打印机:"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,6,0"/>
|
||||
<ComboBox Width="300"
|
||||
Height="34"
|
||||
ItemsSource="{Binding Printers}"
|
||||
SelectedItem="{Binding SelectedPrinter, Mode=TwoWay}"
|
||||
DisplayMemberPath="Name"
|
||||
VerticalContentAlignment="Center"/>
|
||||
<Button Content="刷新打印机"
|
||||
Height="34"
|
||||
Margin="8,0,8,0"
|
||||
Padding="12,0"
|
||||
Click="RefreshPrintersButton_OnClick"/>
|
||||
<TextBlock Text="{Binding PrinterStatus}"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="#595959"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition x:Name="LeftPaneCol" Width="7*"/>
|
||||
<ColumnDefinition Width="10"/>
|
||||
<ColumnDefinition x:Name="RightPaneCol" Width="3*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border Grid.Column="0"
|
||||
BorderBrush="#E5E6EB"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4"
|
||||
Padding="0"
|
||||
Background="#FFFFFF">
|
||||
<DataGrid x:Name="PlanGrid"
|
||||
ItemsSource="{Binding PlanItems}"
|
||||
SelectedItem="{Binding SelectedPlanItem, Mode=TwoWay}"
|
||||
AutoGenerateColumns="False"
|
||||
HeadersVisibility="Column"
|
||||
CanUserResizeColumns="True"
|
||||
CanUserReorderColumns="False"
|
||||
CanUserSortColumns="False"
|
||||
CanUserAddRows="False"
|
||||
CanUserDeleteRows="False"
|
||||
IsReadOnly="True"
|
||||
GridLinesVisibility="None"
|
||||
HorizontalGridLinesBrush="Transparent"
|
||||
VerticalGridLinesBrush="Transparent"
|
||||
BorderThickness="0"
|
||||
RowHeaderWidth="0"
|
||||
SelectionMode="Single"
|
||||
SelectionUnit="FullRow"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Auto"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||
SelectionChanged="PlanGrid_OnSelectionChanged">
|
||||
<DataGrid.Resources>
|
||||
<Style x:Key="CenteredCellTextStyle" TargetType="TextBlock">
|
||||
<Setter Property="HorizontalAlignment" Value="Center"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="TextAlignment" Value="Center"/>
|
||||
</Style>
|
||||
</DataGrid.Resources>
|
||||
<DataGrid.ColumnHeaderStyle>
|
||||
<Style TargetType="DataGridColumnHeader">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="MinHeight" Value="36"/>
|
||||
<Setter Property="Padding" Value="8,6"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
</Style>
|
||||
</DataGrid.ColumnHeaderStyle>
|
||||
<DataGrid.RowStyle>
|
||||
<Style TargetType="DataGridRow">
|
||||
<Setter Property="Margin" Value="0,0,0,3"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="BorderBrush" Value="Transparent"/>
|
||||
<Setter Property="Background" Value="#FFFFFF"/>
|
||||
<Setter Property="SnapsToDevicePixels" Value="True"/>
|
||||
</Style>
|
||||
</DataGrid.RowStyle>
|
||||
<DataGrid.CellStyle>
|
||||
<Style TargetType="DataGridCell">
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="BorderBrush" Value="Transparent"/>
|
||||
<Setter Property="Padding" Value="8,10"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="TextBlock.TextAlignment" Value="Center"/>
|
||||
</Style>
|
||||
</DataGrid.CellStyle>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="#" Binding="{Binding Index}" Width="70" MinWidth="60" ElementStyle="{StaticResource CenteredCellTextStyle}"/>
|
||||
<DataGridTextColumn Header="条码" Binding="{Binding Card.Barcode}" Width="220" MinWidth="170" ElementStyle="{StaticResource CenteredCellTextStyle}"/>
|
||||
<DataGridTextColumn Header="来源行" Binding="{Binding SourceRowNo}" Width="90" MinWidth="70" ElementStyle="{StaticResource CenteredCellTextStyle}"/>
|
||||
<DataGridTextColumn Header="物料名称" Binding="{Binding Card.MaterialName}" Width="220" MinWidth="160" ElementStyle="{StaticResource CenteredCellTextStyle}"/>
|
||||
<DataGridTextColumn Header="批次号" Binding="{Binding Card.BatchNo}" Width="220" MinWidth="160" ElementStyle="{StaticResource CenteredCellTextStyle}"/>
|
||||
<DataGridTextColumn Header="库位" Binding="{Binding Card.WarehouseArea}" Width="150" MinWidth="100" ElementStyle="{StaticResource CenteredCellTextStyle}"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Border>
|
||||
|
||||
<GridSplitter Grid.Column="1"
|
||||
Width="10"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="Transparent"
|
||||
ResizeBehavior="PreviousAndNext"
|
||||
Cursor="SizeWE"
|
||||
DragCompleted="MainSplitter_OnDragCompleted"/>
|
||||
|
||||
<Border Grid.Column="2"
|
||||
BorderBrush="#E5E6EB"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4"
|
||||
Background="#FFFFFF">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel Grid.Row="0" Margin="10,8,10,8">
|
||||
<TextBlock Text="打印模板预览" FontSize="14" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="{Binding TemplateText}" Margin="0,4,0,0" FontSize="12" Foreground="#8C8C8C"/>
|
||||
</StackPanel>
|
||||
<wv2:WebView2 x:Name="PreviewWebView" Grid.Row="1" DefaultBackgroundColor="#FFFFFFFF"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Grid.Row="2"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0,12,0,0">
|
||||
<Button Content="取消"
|
||||
Width="92"
|
||||
Height="34"
|
||||
Margin="0,0,10,0"
|
||||
Click="CancelButton_OnClick"
|
||||
Style="{StaticResource ButtonDefault}"/>
|
||||
<Button Content="生成并打印"
|
||||
Width="120"
|
||||
Height="34"
|
||||
Click="ConfirmButton_OnClick"
|
||||
Style="{StaticResource ButtonPrimary}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</hc:Window>
|
||||
@@ -0,0 +1,389 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using YY.Admin.Core.Services;
|
||||
using YY.Admin.ViewModels.RawMaterialEntry;
|
||||
|
||||
namespace YY.Admin.Views.RawMaterialEntry;
|
||||
|
||||
/// <summary>
|
||||
/// 需显式实现 <see cref="INotifyPropertyChanged"/>,否则 WPF 绑定不会订阅 PropertyChanged,
|
||||
/// 界面会一直停留在属性初始值(例如 PrinterStatus 的「加载打印机中…」)。
|
||||
/// </summary>
|
||||
public partial class RawMaterialCardGenerateConfirmWindow : HandyControl.Controls.Window, INotifyPropertyChanged
|
||||
{
|
||||
private const int PrinterLoadTimeoutMs = 8000;
|
||||
private const double DefaultLeftRatio = 0.7d;
|
||||
private const double MinLeftRatio = 0.2d;
|
||||
private const double MaxLeftRatio = 0.8d;
|
||||
private static readonly JsonSerializerOptions LayoutJsonOpts = new() { WriteIndented = true };
|
||||
private static string LayoutFilePath => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"YY.Admin",
|
||||
"raw-material-card-generate-confirm-layout.json");
|
||||
|
||||
public ObservableCollection<RawMaterialCardGeneratePlanRow> PlanItems { get; } = new();
|
||||
|
||||
public string HeaderText { get; }
|
||||
public string TemplateText { get; }
|
||||
public ObservableCollection<PrintDotPrinter> Printers { get; } = new();
|
||||
private readonly Func<RawMaterialCardGeneratePlanRow, string> _previewHtmlBuilder;
|
||||
private readonly IPrintDotService _printDotService;
|
||||
private bool _webViewReady;
|
||||
private bool _isRefreshingPrinters;
|
||||
private bool _suppressPrinterSave;
|
||||
private string? _preferredPrinterNameOnLoad;
|
||||
|
||||
private string _printerStatus = "加载打印机中...";
|
||||
public string PrinterStatus
|
||||
{
|
||||
get => _printerStatus;
|
||||
set
|
||||
{
|
||||
if (string.Equals(_printerStatus, value, StringComparison.Ordinal)) return;
|
||||
_printerStatus = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PrinterStatus)));
|
||||
}
|
||||
}
|
||||
|
||||
private PrintDotPrinter? _selectedPrinter;
|
||||
public PrintDotPrinter? SelectedPrinter
|
||||
{
|
||||
get => _selectedPrinter;
|
||||
set
|
||||
{
|
||||
if (ReferenceEquals(_selectedPrinter, value)) return;
|
||||
_selectedPrinter = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedPrinter)));
|
||||
if (_suppressPrinterSave) return;
|
||||
SaveLayout(dto => dto.SelectedPrinterName = value?.Name ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public string SelectedPrinterName => SelectedPrinter?.Name?.Trim() ?? string.Empty;
|
||||
|
||||
private RawMaterialCardGeneratePlanRow? _selectedPlanItem;
|
||||
public RawMaterialCardGeneratePlanRow? SelectedPlanItem
|
||||
{
|
||||
get => _selectedPlanItem;
|
||||
set
|
||||
{
|
||||
if (ReferenceEquals(_selectedPlanItem, value)) return;
|
||||
_selectedPlanItem = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedPlanItem)));
|
||||
}
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public RawMaterialCardGenerateConfirmWindow(
|
||||
IReadOnlyList<RawMaterialEntryOperationViewModel.RawMaterialCardGeneratePlanItem> planItems,
|
||||
string templateName,
|
||||
string templateCode,
|
||||
IPrintDotService printDotService,
|
||||
Func<RawMaterialCardGeneratePlanRow, string> previewHtmlBuilder)
|
||||
{
|
||||
InitializeComponent();
|
||||
_printDotService = printDotService;
|
||||
_previewHtmlBuilder = previewHtmlBuilder;
|
||||
HeaderText = $"共 {planItems.Count} 张,左侧展示即将生成的原材料卡片,右侧展示业务关联打印模板预览。";
|
||||
TemplateText = $"模板:{templateName}({templateCode})";
|
||||
var idx = 0;
|
||||
foreach (var item in planItems)
|
||||
{
|
||||
idx++;
|
||||
PlanItems.Add(new RawMaterialCardGeneratePlanRow
|
||||
{
|
||||
Index = idx,
|
||||
SourceRowNo = item.SourceRowNo,
|
||||
DetailId = item.DetailId,
|
||||
Card = item.Card
|
||||
});
|
||||
}
|
||||
|
||||
DataContext = this;
|
||||
Loaded += OnLoadedAsync;
|
||||
Closing += OnClosing;
|
||||
}
|
||||
|
||||
private async void OnLoadedAsync(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var layout = LoadSavedLayout();
|
||||
ApplyPaneRatio(layout.LeftPaneRatio ?? DefaultLeftRatio);
|
||||
ApplySavedColumnWidths(layout.ColumnWidths);
|
||||
_preferredPrinterNameOnLoad = layout.SelectedPrinterName;
|
||||
await RefreshPrintersAsync(verbose: false);
|
||||
try
|
||||
{
|
||||
await PreviewWebView.EnsureCoreWebView2Async();
|
||||
_webViewReady = true;
|
||||
if (SelectedPlanItem == null && PlanItems.Count > 0)
|
||||
{
|
||||
SelectedPlanItem = PlanItems[0];
|
||||
}
|
||||
RenderSelectedPreview();
|
||||
}
|
||||
catch
|
||||
{
|
||||
PreviewWebView.NavigateToString("<html><body style='font-family:Microsoft YaHei;padding:24px;'>模板预览加载失败</body></html>");
|
||||
}
|
||||
}
|
||||
|
||||
private async void RefreshPrintersButton_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await RefreshPrintersAsync(verbose: true);
|
||||
}
|
||||
|
||||
private void MainSplitter_OnDragCompleted(object sender, DragCompletedEventArgs e)
|
||||
{
|
||||
SavePaneRatio(GetCurrentLeftRatio());
|
||||
}
|
||||
|
||||
private double GetCurrentLeftRatio()
|
||||
{
|
||||
var left = LeftPaneCol.ActualWidth;
|
||||
var right = RightPaneCol.ActualWidth;
|
||||
var total = left + right;
|
||||
if (total <= 0.1d) return DefaultLeftRatio;
|
||||
return Math.Clamp(left / total, MinLeftRatio, MaxLeftRatio);
|
||||
}
|
||||
|
||||
private void ApplyPaneRatio(double ratio)
|
||||
{
|
||||
var leftRatio = Math.Clamp(ratio, MinLeftRatio, MaxLeftRatio);
|
||||
LeftPaneCol.Width = new GridLength(leftRatio, GridUnitType.Star);
|
||||
RightPaneCol.Width = new GridLength(1d - leftRatio, GridUnitType.Star);
|
||||
}
|
||||
|
||||
private static ConfirmWindowLayoutDto LoadSavedLayout()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(LayoutFilePath)) return new ConfirmWindowLayoutDto();
|
||||
var json = File.ReadAllText(LayoutFilePath);
|
||||
var dto = JsonSerializer.Deserialize<ConfirmWindowLayoutDto>(json, LayoutJsonOpts);
|
||||
if (dto is null) return new ConfirmWindowLayoutDto();
|
||||
if (dto.LeftPaneRatio is > 0 and < 1)
|
||||
dto.LeftPaneRatio = Math.Clamp(dto.LeftPaneRatio.Value, MinLeftRatio, MaxLeftRatio);
|
||||
return dto;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 布局缓存异常时使用默认比例,不影响主流程
|
||||
}
|
||||
return new ConfirmWindowLayoutDto();
|
||||
}
|
||||
|
||||
private static void SavePaneRatio(double leftRatio)
|
||||
{
|
||||
SaveLayout(dto => { dto.LeftPaneRatio = Math.Clamp(leftRatio, MinLeftRatio, MaxLeftRatio); });
|
||||
}
|
||||
|
||||
private static void SaveLayout(Action<ConfirmWindowLayoutDto> mutator)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dir = Path.GetDirectoryName(LayoutFilePath);
|
||||
if (!string.IsNullOrWhiteSpace(dir))
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
|
||||
var dto = LoadSavedLayout();
|
||||
mutator(dto);
|
||||
File.WriteAllText(LayoutFilePath, JsonSerializer.Serialize(dto, LayoutJsonOpts));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 写入失败不阻断页面交互
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplySavedColumnWidths(Dictionary<string, double>? columnWidths)
|
||||
{
|
||||
if (columnWidths is null || columnWidths.Count == 0) return;
|
||||
for (var index = 0; index < PlanGrid.Columns.Count; index++)
|
||||
{
|
||||
var column = PlanGrid.Columns[index];
|
||||
var key = GetColumnWidthKey(column, index);
|
||||
if (!columnWidths.TryGetValue(key, out var cachedWidth) || cachedWidth <= 0) continue;
|
||||
var minWidth = column.MinWidth > 0 ? column.MinWidth : 40d;
|
||||
column.Width = new System.Windows.Controls.DataGridLength(
|
||||
Math.Max(cachedWidth, minWidth),
|
||||
System.Windows.Controls.DataGridLengthUnitType.Pixel);
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveCurrentColumnWidths()
|
||||
{
|
||||
if (PlanGrid.Columns.Count == 0) return;
|
||||
var widths = new Dictionary<string, double>(StringComparer.Ordinal);
|
||||
for (var index = 0; index < PlanGrid.Columns.Count; index++)
|
||||
{
|
||||
var column = PlanGrid.Columns[index];
|
||||
var width = column.ActualWidth;
|
||||
if (double.IsNaN(width) || double.IsInfinity(width) || width <= 0) continue;
|
||||
widths[GetColumnWidthKey(column, index)] = width;
|
||||
}
|
||||
|
||||
if (widths.Count == 0) return;
|
||||
SaveLayout(dto => dto.ColumnWidths = widths);
|
||||
}
|
||||
|
||||
private static string GetColumnWidthKey(System.Windows.Controls.DataGridColumn column, int index)
|
||||
{
|
||||
var header = column.Header?.ToString();
|
||||
if (string.IsNullOrWhiteSpace(header))
|
||||
return $"col-{index.ToString(CultureInfo.InvariantCulture)}";
|
||||
return $"col-{index.ToString(CultureInfo.InvariantCulture)}-{header.Trim()}";
|
||||
}
|
||||
|
||||
private void OnClosing(object? sender, CancelEventArgs e)
|
||||
{
|
||||
SavePaneRatio(GetCurrentLeftRatio());
|
||||
SaveCurrentColumnWidths();
|
||||
}
|
||||
|
||||
private async Task RefreshPrintersAsync(bool verbose)
|
||||
{
|
||||
if (_isRefreshingPrinters) return;
|
||||
_isRefreshingPrinters = true;
|
||||
const string loadingText = "加载打印机中...";
|
||||
PrinterStatus = verbose ? "刷新打印机中..." : loadingText;
|
||||
try
|
||||
{
|
||||
using var cts = new CancellationTokenSource();
|
||||
var fetchTask = _printDotService.GetPrintersAsync(cts.Token);
|
||||
var timeoutTask = Task.Delay(PrinterLoadTimeoutMs);
|
||||
var completedTask = await Task.WhenAny(fetchTask, timeoutTask);
|
||||
if (!ReferenceEquals(completedTask, fetchTask))
|
||||
{
|
||||
cts.Cancel();
|
||||
throw new TimeoutException("获取打印机列表超时");
|
||||
}
|
||||
|
||||
var list = await fetchTask;
|
||||
Printers.Clear();
|
||||
foreach (var printer in list) Printers.Add(printer);
|
||||
// 先更新状态,避免后续本地缓存逻辑异常导致“加载中”残留
|
||||
PrinterStatus = list.Count > 0 ? $"共 {list.Count} 台打印机" : "未检测到打印机";
|
||||
|
||||
var preferred = _preferredPrinterNameOnLoad;
|
||||
if (string.IsNullOrWhiteSpace(preferred))
|
||||
{
|
||||
preferred = SelectedPrinter?.Name;
|
||||
}
|
||||
|
||||
var match = list.FirstOrDefault(p => string.Equals(p.Name, preferred, StringComparison.OrdinalIgnoreCase))
|
||||
?? list.FirstOrDefault(p => p.IsDefault)
|
||||
?? list.FirstOrDefault();
|
||||
|
||||
_suppressPrinterSave = true;
|
||||
SelectedPrinter = match;
|
||||
_suppressPrinterSave = false;
|
||||
|
||||
if (match is not null)
|
||||
{
|
||||
SaveLayout(dto => dto.SelectedPrinterName = match.Name);
|
||||
}
|
||||
_preferredPrinterNameOnLoad = null;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Printers.Clear();
|
||||
_suppressPrinterSave = true;
|
||||
SelectedPrinter = null;
|
||||
_suppressPrinterSave = false;
|
||||
PrinterStatus = "加载打印机超时,请检查 PrintDot 后重试";
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
Printers.Clear();
|
||||
_suppressPrinterSave = true;
|
||||
SelectedPrinter = null;
|
||||
_suppressPrinterSave = false;
|
||||
PrinterStatus = "加载打印机超时,请检查 PrintDot 后重试";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Printers.Clear();
|
||||
_suppressPrinterSave = true;
|
||||
SelectedPrinter = null;
|
||||
_suppressPrinterSave = false;
|
||||
PrinterStatus = verbose ? $"打印机连接失败:{ex.Message}" : "打印机连接失败";
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 兜底:如果实际有打印机但状态仍是“加载中”,强制纠正状态文本
|
||||
if (string.Equals(PrinterStatus, loadingText, StringComparison.Ordinal) && Printers.Count > 0)
|
||||
{
|
||||
PrinterStatus = $"共 {Printers.Count} 台打印机";
|
||||
}
|
||||
_isRefreshingPrinters = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void PlanGrid_OnSelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
|
||||
{
|
||||
RenderSelectedPreview();
|
||||
}
|
||||
|
||||
private void RenderSelectedPreview()
|
||||
{
|
||||
if (!_webViewReady) return;
|
||||
if (SelectedPlanItem == null)
|
||||
{
|
||||
PreviewWebView.NavigateToString("<html><body style='font-family:Microsoft YaHei;padding:24px;'>请先选择左侧卡片记录</body></html>");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var html = _previewHtmlBuilder.Invoke(SelectedPlanItem);
|
||||
PreviewWebView.NavigateToString(html);
|
||||
}
|
||||
catch
|
||||
{
|
||||
PreviewWebView.NavigateToString("<html><body style='font-family:Microsoft YaHei;padding:24px;'>模板预览加载失败</body></html>");
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelButton_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DialogResult = false;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void ConfirmButton_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(SelectedPrinterName))
|
||||
{
|
||||
HandyControl.Controls.MessageBox.Warning("请先在当前弹窗选择打印机,再执行“生成并打印”。");
|
||||
return;
|
||||
}
|
||||
DialogResult = true;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ConfirmWindowLayoutDto
|
||||
{
|
||||
public double? LeftPaneRatio { get; set; }
|
||||
public Dictionary<string, double>? ColumnWidths { get; set; }
|
||||
public string? SelectedPrinterName { get; set; }
|
||||
}
|
||||
|
||||
public sealed class RawMaterialCardGeneratePlanRow
|
||||
{
|
||||
public int Index { get; init; }
|
||||
public int SourceRowNo { get; init; }
|
||||
public required string DetailId { get; init; }
|
||||
public required YY.Admin.Core.Entity.MesXslRawMaterialCard Card { get; init; }
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:ctrls="clr-namespace:YY.Admin.Controls"
|
||||
xmlns:core="clr-namespace:YY.Admin.Core.Entity;assembly=YY.Admin.Core"
|
||||
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
||||
mc:Ignorable="d">
|
||||
@@ -38,6 +39,7 @@
|
||||
|
||||
<Grid Style="{StaticResource BaseViewStyle}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
@@ -73,7 +75,30 @@
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1" x:Name="MainSplitRoot">
|
||||
<!-- 打印机:PrintDot 桥接器,与「打印模板」页一致 -->
|
||||
<Border Grid.Row="1" Margin="24,0,24,8" Padding="0,4,0,4" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="打印机:" VerticalAlignment="Center" FontSize="13" Foreground="#333333" Margin="0,0,8,0"/>
|
||||
<ComboBox ItemsSource="{Binding Printers}"
|
||||
SelectedItem="{Binding SelectedPrinter}"
|
||||
DisplayMemberPath="Name"
|
||||
MinWidth="220" Height="30"
|
||||
VerticalContentAlignment="Center"
|
||||
hc:InfoElement.Placeholder="请选择打印机"/>
|
||||
<Button Command="{Binding RefreshPrintersCommand}"
|
||||
Height="30" Padding="10,0" Margin="10,0,0,0"
|
||||
FontSize="12"
|
||||
Style="{StaticResource ButtonDefault}"
|
||||
Content="刷新打印机"/>
|
||||
<TextBlock Text="{Binding PrinterStatus}"
|
||||
Margin="12,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Row="2" x:Name="MainSplitRoot">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" MinWidth="320"/>
|
||||
<ColumnDefinition x:Name="SplitterCol" Width="4"/>
|
||||
@@ -893,7 +918,13 @@
|
||||
BorderBrush="{DynamicResource BorderBrush}"
|
||||
BorderThickness="1,0,0,0"
|
||||
Background="{DynamicResource RegionBrush}">
|
||||
<DockPanel LastChildFill="True">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" MinHeight="120"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<DockPanel Grid.Row="0" LastChildFill="True">
|
||||
|
||||
<!-- ===== 标题 + 日期筛选 ===== -->
|
||||
<Border DockPanel.Dock="Top" BorderBrush="{DynamicResource BorderBrush}"
|
||||
@@ -998,12 +1029,134 @@
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
</DockPanel>
|
||||
</DockPanel>
|
||||
|
||||
<!-- ===== 下方:入场标签实时打印预览(可折叠,仅画布无 JSON) ===== -->
|
||||
<StackPanel Grid.Row="1" Margin="0,4,0,0" VerticalAlignment="Bottom">
|
||||
<Border BorderBrush="{DynamicResource BorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="6,6,0,0"
|
||||
Background="{DynamicResource SecondaryRegionBrush}"
|
||||
Padding="10,8">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="入场标签打印预览"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="12"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding PrintPreviewStatus}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
FontSize="10"
|
||||
Opacity="0.72"
|
||||
Margin="8,0,10,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}"/>
|
||||
<StackPanel Grid.Column="2"
|
||||
Orientation="Horizontal"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Right">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{Binding IsPrintPreviewExpanded, Converter={StaticResource Boolean2VisibilityConverter}}">
|
||||
<Button Content="-"
|
||||
Width="24"
|
||||
Height="24"
|
||||
Padding="0"
|
||||
FontSize="12"
|
||||
Command="{Binding ZoomOutPrintPreviewCommand}"
|
||||
Style="{StaticResource ButtonDefault}"
|
||||
ToolTip="缩小预览"/>
|
||||
<Button Content="{Binding PrintPreviewZoomText}"
|
||||
MinWidth="56"
|
||||
Height="24"
|
||||
Margin="4,0,4,0"
|
||||
Padding="10,0"
|
||||
FontSize="11"
|
||||
Command="{Binding ResetPrintPreviewZoomCommand}"
|
||||
Style="{StaticResource ButtonDefault}"
|
||||
ToolTip="重置为 70%"/>
|
||||
<Button Content="+"
|
||||
Width="24"
|
||||
Height="24"
|
||||
Padding="0"
|
||||
FontSize="12"
|
||||
Command="{Binding ZoomInPrintPreviewCommand}"
|
||||
Style="{StaticResource ButtonDefault}"
|
||||
ToolTip="放大预览"/>
|
||||
</StackPanel>
|
||||
<ToggleButton IsChecked="{Binding IsPrintPreviewExpanded, Mode=TwoWay}"
|
||||
MinWidth="56"
|
||||
Height="24"
|
||||
Background="Transparent"
|
||||
BorderThickness="1"
|
||||
Padding="8,0"
|
||||
FocusVisualStyle="{x:Null}"
|
||||
Cursor="Hand"
|
||||
VerticalAlignment="Center">
|
||||
<ToggleButton.Template>
|
||||
<ControlTemplate TargetType="ToggleButton">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
BorderBrush="{DynamicResource BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="2"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</ToggleButton.Template>
|
||||
<TextBlock FontSize="12"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Text" Value="展开"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsChecked, RelativeSource={RelativeSource AncestorType=ToggleButton}}" Value="True">
|
||||
<Setter Property="Text" Value="折叠"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
<Border BorderBrush="{DynamicResource BorderBrush}"
|
||||
BorderThickness="1,0,1,1"
|
||||
CornerRadius="0,0,2,2"
|
||||
Padding="0"
|
||||
MinHeight="200"
|
||||
MaxHeight="420"
|
||||
Background="#525659"
|
||||
Visibility="{Binding IsPrintPreviewExpanded, Converter={StaticResource Boolean2VisibilityConverter}}">
|
||||
<wv2:WebView2 x:Name="PrintPreviewWebView"
|
||||
DefaultBackgroundColor="#FF525659"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,12,0,20">
|
||||
<Button Content="保存" Command="{Binding SaveCommand}" Style="{StaticResource ButtonPrimary}" Width="100" Margin="0,0,15,0"/>
|
||||
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,12,0,20">
|
||||
<Button Content="保存并打印入场记录"
|
||||
Command="{Binding SaveAndPrintCommand}"
|
||||
Style="{StaticResource ButtonPrimary}"
|
||||
IsEnabled="{Binding IsNotActionBusy}"
|
||||
Width="168" Margin="0,0,15,0"
|
||||
ToolTip="保存成功后直接发送到已选打印机(不弹预览窗口)"/>
|
||||
<Button Content="保存"
|
||||
Command="{Binding SaveCommand}"
|
||||
Style="{StaticResource ButtonDefault}"
|
||||
IsEnabled="{Binding IsNotActionBusy}"
|
||||
Width="100" Margin="0,0,15,0"/>
|
||||
<Button Content="生成原材料卡片"
|
||||
Command="{Binding GenerateRawMaterialCardsCommand}"
|
||||
Style="{StaticResource ButtonDefault}"
|
||||
@@ -1011,5 +1164,29 @@
|
||||
IsEnabled="{Binding CanGenerateCards}"
|
||||
ToolTip="根据拆码明细生成原材料卡片(需先保存入场记录)"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 底部动作按钮忙碌遮罩:保存/保存并打印/生成时显示,禁止重复点击 -->
|
||||
<Border Grid.RowSpan="4"
|
||||
Panel.ZIndex="999"
|
||||
Background="#66000000"
|
||||
Visibility="{Binding IsActionBusy, Converter={StaticResource Boolean2VisibilityConverter}}">
|
||||
<Border Width="220"
|
||||
Height="120"
|
||||
CornerRadius="8"
|
||||
Background="#F2FFFFFF"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Padding="16">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<hc:LoadingCircle Width="42" Height="42"/>
|
||||
<TextBlock Text="{Binding ActionBusyText}"
|
||||
Margin="0,14,0,0"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"
|
||||
HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Border>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -26,6 +26,8 @@ public partial class RawMaterialEntryOperationView : UserControl
|
||||
{
|
||||
_vm = vm;
|
||||
vm.PropertyChanged += OnVmPropertyChanged;
|
||||
vm.PrintPreviewHtmlReady += OnPrintPreviewHtmlReady;
|
||||
vm.StartPrintPreviewTimer();
|
||||
}
|
||||
|
||||
ApplySplitLayout();
|
||||
@@ -36,6 +38,8 @@ public partial class RawMaterialEntryOperationView : UserControl
|
||||
if (_vm != null)
|
||||
{
|
||||
_vm.PropertyChanged -= OnVmPropertyChanged;
|
||||
_vm.PrintPreviewHtmlReady -= OnPrintPreviewHtmlReady;
|
||||
_vm.StopPrintPreviewTimer();
|
||||
_vm = null;
|
||||
}
|
||||
}
|
||||
@@ -45,10 +49,22 @@ public partial class RawMaterialEntryOperationView : UserControl
|
||||
// null/空 表示“所有属性”通知(Prism/BindableBase 批量刷新时),需同步分割布局
|
||||
if (!string.IsNullOrEmpty(e.PropertyName)
|
||||
&& e.PropertyName is not nameof(RawMaterialEntryOperationViewModel.IsRightPanelExpanded)
|
||||
&& e.PropertyName is not nameof(RawMaterialEntryOperationViewModel.ExpandedRightPanelWidth))
|
||||
&& e.PropertyName is not nameof(RawMaterialEntryOperationViewModel.ExpandedRightPanelWidth)
|
||||
&& e.PropertyName is not nameof(RawMaterialEntryOperationViewModel.PrintPreviewZoomFactor))
|
||||
return;
|
||||
|
||||
_ = Dispatcher.InvokeAsync(ApplySplitLayout);
|
||||
if (string.IsNullOrEmpty(e.PropertyName)
|
||||
|| e.PropertyName is nameof(RawMaterialEntryOperationViewModel.IsRightPanelExpanded)
|
||||
|| e.PropertyName is nameof(RawMaterialEntryOperationViewModel.ExpandedRightPanelWidth))
|
||||
{
|
||||
_ = Dispatcher.InvokeAsync(ApplySplitLayout);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(e.PropertyName)
|
||||
|| e.PropertyName is nameof(RawMaterialEntryOperationViewModel.PrintPreviewZoomFactor))
|
||||
{
|
||||
_ = Dispatcher.InvokeAsync(ApplyPrintPreviewZoom);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>按钮 Click 在 Command 之后执行,用于兜底刷新列宽(不重复切换状态)。</summary>
|
||||
@@ -62,6 +78,7 @@ public partial class RawMaterialEntryOperationView : UserControl
|
||||
EnsureVmAttached();
|
||||
|
||||
ApplySplitLayout();
|
||||
ApplyPrintPreviewZoom();
|
||||
|
||||
if (DataContext is RawMaterialEntryOperationViewModel vm && !_initialized)
|
||||
{
|
||||
@@ -78,9 +95,34 @@ public partial class RawMaterialEntryOperationView : UserControl
|
||||
{
|
||||
_vm = vm;
|
||||
vm.PropertyChanged += OnVmPropertyChanged;
|
||||
vm.PrintPreviewHtmlReady += OnPrintPreviewHtmlReady;
|
||||
vm.StartPrintPreviewTimer();
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnPrintPreviewHtmlReady(object? sender, string html)
|
||||
{
|
||||
try
|
||||
{
|
||||
await PrintPreviewWebView.EnsureCoreWebView2Async();
|
||||
ApplyPrintPreviewZoom();
|
||||
PrintPreviewWebView.NavigateToString(html ?? string.Empty);
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* WebView2 未就绪或宿主已释放时忽略 */
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyPrintPreviewZoom()
|
||||
{
|
||||
var vm = _vm ?? DataContext as RawMaterialEntryOperationViewModel;
|
||||
if (vm == null) return;
|
||||
if (PrintPreviewWebView?.CoreWebView2 == null) return;
|
||||
// 实时预览已关闭 HTML 内部 fitPage,自定义缩放直接映射到 WebView2 即可(+ 放大,- 缩小)。
|
||||
PrintPreviewWebView.ZoomFactor = vm.PrintPreviewZoomFactor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据 ViewModel 同步右侧栏与分割条:展开时使用持久化宽度;折叠时右栏与分割条占宽均为 0(完全隐藏)。
|
||||
/// </summary>
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3065.39" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http">
|
||||
<Version>10.0.7</Version>
|
||||
</PackageReference>
|
||||
|
||||
Reference in New Issue
Block a user