增强应用程序异常处理机制,新增未处理异常日志记录功能,确保在启动和运行期间捕获并记录异常信息。同时,重构配置文件加载逻辑,支持用户目录覆盖默认配置,优化 SQLite 数据库连接字符串处理,确保在不同环境下的兼容性和稳定性。

This commit is contained in:
geht
2026-05-15 17:30:30 +08:00
parent cea4781397
commit 1b280af9e2
23 changed files with 584 additions and 68 deletions

Binary file not shown.

View File

@@ -10,6 +10,7 @@ using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data; using System.Data;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Linq; using System.Linq;
using System.Linq.Dynamic.Core; using System.Linq.Dynamic.Core;
using System.Reflection; using System.Reflection;
@@ -19,7 +20,9 @@ using Yitter.IdGenerator;
using YY.Admin.Core; using YY.Admin.Core;
using YY.Admin.Core.Extension; using YY.Admin.Core.Extension;
using YY.Admin.Core.Option; using YY.Admin.Core.Option;
using YY.Admin.Core.SeedData;
using YY.Admin.Core.Session; using YY.Admin.Core.Session;
using YY.Admin.Core.Util;
using DbType = SqlSugar.DbType; using DbType = SqlSugar.DbType;
namespace YY.Admin.Core.SqlSugar namespace YY.Admin.Core.SqlSugar
@@ -160,6 +163,12 @@ namespace YY.Admin.Core.SqlSugar
config.ConnectionString = CryptogramUtil.Decrypt(config.ConnectionString); config.ConnectionString = CryptogramUtil.Decrypt(config.ConnectionString);
} }
// SQLite 相对路径默认依赖进程工作目录;快捷方式/从不同目录启动会在别处生成空库,表现为发布后菜单全空
if (config.DbType == DbType.Sqlite && !string.IsNullOrWhiteSpace(config.ConnectionString))
{
config.ConnectionString = ResolveSqliteConnectionToAbsolutePath(config.ConnectionString);
}
var configureExternalServices = new ConfigureExternalServices var configureExternalServices = new ConfigureExternalServices
{ {
EntityNameService = (type, entity) => // 处理表 EntityNameService = (type, entity) => // 处理表
@@ -250,6 +259,96 @@ namespace YY.Admin.Core.SqlSugar
//if (config.DbSettings.EnableInitView) InitView(dbProvider); //if (config.DbSettings.EnableInitView) InitView(dbProvider);
// 初始化种子数据 // 初始化种子数据
if (config.SeedSettings.EnableInitSeed) InitSeedData(db, config); if (config.SeedSettings.EnableInitSeed) InitSeedData(db, config);
// 关闭全量种子时首启可能无菜单数据;补一份基准菜单,避免打包版本左侧空白
EnsureBaselineSysMenuSeed(db, config);
}
/// <summary>
/// 将 SQLite 连接串中的相对 DataSource 解析为基于应用程序基目录的绝对路径。
/// </summary>
private static string ResolveSqliteConnectionToAbsolutePath(string connectionString)
{
try
{
var builder = new SqliteConnectionStringBuilder(connectionString);
var ds = builder.DataSource;
if (string.IsNullOrWhiteSpace(ds))
{
return connectionString;
}
if (Path.IsPathFullyQualified(ds))
{
return connectionString;
}
// Program Files 等安装目录对普通用户只读SQLite 库放到 LocalApplicationData
var dataDir = AppWritablePaths.EnsureDirectoryExists(AppWritablePaths.DataDirectory);
var fileName = Path.GetFileName(ds.TrimStart('.', '/', '\\'));
if (string.IsNullOrWhiteSpace(fileName))
{
fileName = "Admin.NET.db";
}
builder.DataSource = Path.Combine(dataDir, fileName);
return builder.ConnectionString;
}
catch
{
return connectionString;
}
}
/// <summary>
/// 未启用 EnableInitSeed 且 sys_menu 为空时,写入 SysMenu 种子,避免发布后界面无功能菜单。
/// </summary>
private static void EnsureBaselineSysMenuSeed(SqlSugarScope db, DbConnectionConfig config)
{
try
{
if (config.SeedSettings.EnableInitSeed)
{
return;
}
if (!string.Equals(config.ConfigId.ToString(), SqlSugarConst.MainConfigId, StringComparison.Ordinal))
{
return;
}
if (config.DbType != DbType.Sqlite)
{
return;
}
var dbProvider = db.GetConnectionScope(config.ConfigId);
var menuEntityInfo = dbProvider.EntityMaintenance.GetEntityInfo(typeof(SysMenu));
if (!dbProvider.DbMaintenance.IsAnyTable(menuEntityInfo.DbTableName, false))
{
return;
}
var cnt = dbProvider.Queryable<SysMenu>().ClearFilter().Count();
if (cnt > 0)
{
return;
}
var seedType = typeof(SysMenuSeedData);
var seedData = GetSeedData(seedType)?.ToList();
if (seedData == null || seedData.Count == 0)
{
return;
}
AdjustSeedDataIds(seedData, config);
var progress = 0;
InsertOrUpdateSeedData(dbProvider, seedType, typeof(SysMenu), seedData, config, ref progress, 1);
}
catch
{
// 启动阶段不因兜底种子失败而阻断
}
} }
/// <summary> /// <summary>

View File

@@ -0,0 +1,38 @@
using System.IO;
namespace YY.Admin.Core.Util;
/// <summary>
/// 当前用户可写应用数据目录(避免安装在 Program Files 时无写权限)。
/// </summary>
public static class AppWritablePaths
{
private static readonly string Root = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"YY.Admin");
/// <summary>应用私有根目录:%LocalAppData%\YY.Admin</summary>
public static string LocalApplicationRoot => Root;
/// <summary>SQLite 等业务数据库目录。</summary>
public static string DataDirectory => Path.Combine(Root, "Data");
/// <summary>用户覆盖的配置、Jeecg 同步状态等。</summary>
public static string ConfigurationDirectory => Path.Combine(Root, "Configuration");
/// <summary>按账号划分的本地设置(对应 CommonConst.AppSettingsFilePath 前缀)。</summary>
public static string AccountSettingsRootDirectory => Path.Combine(Root, "AppSettings");
/// <summary>
/// 创建目录(若不存在)并返回路径。
/// </summary>
public static string EnsureDirectoryExists(string directoryPath)
{
if (!string.IsNullOrWhiteSpace(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}
return directoryPath;
}
}

View File

@@ -3,6 +3,7 @@ using System.Net.Http;
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using YY.Admin.Core.Util;
namespace YY.Admin.Services.Service.Jeecg; namespace YY.Admin.Services.Service.Jeecg;
@@ -226,7 +227,8 @@ public class JeecgLoginLogReportService : IJeecgLoginLogReportService, IClientLo
private static string GetQueuePath() private static string GetQueuePath()
{ {
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "AppSettings", "offline-scada-log-queue.jsonl"); var dir = AppWritablePaths.EnsureDirectoryExists(AppWritablePaths.AccountSettingsRootDirectory);
return Path.Combine(dir, "offline-scada-log-queue.jsonl");
} }
private static void EnsureQueueDir(string path) private static void EnsureQueueDir(string path)

View File

@@ -1,10 +1,11 @@
using System.IO; using System.IO;
using System.Text.Json; using System.Text.Json;
using YY.Admin.Core.Util;
namespace YY.Admin.Services.Service.Jeecg namespace YY.Admin.Services.Service.Jeecg
{ {
/// <summary> /// <summary>
/// 读写本地 Jeecg 同步状态文件(与 appsettings 同目录下的 Configuration /// 读写本地 Jeecg 同步状态文件(用户可写目录,避免 Program Files 无写权限)。
/// </summary> /// </summary>
public class JeecgSyncStateStore public class JeecgSyncStateStore
{ {
@@ -12,7 +13,7 @@ namespace YY.Admin.Services.Service.Jeecg
public JeecgSyncStateStore() public JeecgSyncStateStore()
{ {
var dir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Configuration"); var dir = AppWritablePaths.EnsureDirectoryExists(AppWritablePaths.ConfigurationDirectory);
_filePath = Path.Combine(dir, "jeecg-sync-state.json"); _filePath = Path.Combine(dir, "jeecg-sync-state.json");
} }

View File

@@ -4,7 +4,9 @@ using Mapster;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using NewLife; using NewLife;
using System.IO; using System.IO;
using System.Text;
using System.Windows; using System.Windows;
using System.Windows.Threading;
using YY.Admin.Core; using YY.Admin.Core;
using YY.Admin.EventBus; using YY.Admin.EventBus;
using YY.Admin.Filter; using YY.Admin.Filter;
@@ -14,6 +16,7 @@ using YY.Admin.Services.Service.Print;
using YY.Admin.Setup; using YY.Admin.Setup;
using YY.Admin.ViewModels; using YY.Admin.ViewModels;
using YY.Admin.Views; using YY.Admin.Views;
using YY.Admin.Core.Util;
namespace YY.Admin namespace YY.Admin
{ {
/// <summary> /// <summary>
@@ -24,32 +27,116 @@ namespace YY.Admin
private IConfiguration? _configuration; private IConfiguration? _configuration;
private ILoggerService? _logger; private ILoggerService? _logger;
private readonly SyncModule _syncModule = new(); private readonly SyncModule _syncModule = new();
public App()
{
DispatcherUnhandledException += OnDispatcherUnhandledException;
AppDomain.CurrentDomain.UnhandledException += OnUnhandledDomainException;
}
private static void TryWriteCrashLog(string headline, Exception ex)
{
try
{
var dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "YY.Admin", "logs");
Directory.CreateDirectory(dir);
var file = Path.Combine(dir, $"crash-{DateTime.Now:yyyyMMdd-HHmmss}.txt");
var sb = new StringBuilder();
sb.AppendLine(headline);
sb.AppendLine(ex.ToString());
File.WriteAllText(file, sb.ToString(), Encoding.UTF8);
MessageBox.Show($"程序异常已写入日志:\n{file}\n\n{ex.Message}", "智能制造MES工控", MessageBoxButton.OK, MessageBoxImage.Error);
}
catch
{
MessageBox.Show($"{headline}\n{ex}", "智能制造MES工控", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
TryWriteCrashLog("UI 线程未处理异常", e.Exception);
e.Handled = true;
Shutdown(1);
}
private void OnUnhandledDomainException(object? sender, UnhandledExceptionEventArgs e)
{
if (e.ExceptionObject is Exception ex)
{
TryWriteCrashLog("域未处理异常", ex);
}
if (e.IsTerminating)
{
Environment.Exit(1);
}
}
protected override Window CreateShell() protected override Window CreateShell()
{ {
return Container.Resolve<LoginWindow>(); return Container.Resolve<LoginWindow>();
} }
protected override void OnStartup(StartupEventArgs e) protected override void OnStartup(StartupEventArgs e)
{ {
var baseDirectory = AppDomain.CurrentDomain.BaseDirectory; try
// 构建配置 {
_configuration = new ConfigurationBuilder() var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
.SetBasePath(baseDirectory) var cfgPath = Path.Combine(baseDirectory, "Configuration", "appsettings.json");
.AddJsonFile("Configuration/appsettings.json", optional: false, reloadOnChange: true) if (!File.Exists(cfgPath))
.Build(); {
// 全局配置 var msg = $"未找到配置文件(请确认与 YY.Admin.exe 同目录存在 Configuration\\appsettings.json\n{cfgPath}";
TypeAdapterConfig.GlobalSettings.Default TryWriteStartupFailure(msg);
.IgnoreNullValues(true) Shutdown(1);
.NameMatchingStrategy(NameMatchingStrategy.IgnoreCase); return;
}
// FluentValidation 全局规则级别配置 var userCfgPath = Path.Combine(
ValidatorOptions.Global.DefaultRuleLevelCascadeMode = CascadeMode.Stop; AppWritablePaths.EnsureDirectoryExists(AppWritablePaths.ConfigurationDirectory),
"appsettings.json");
// Mapster 全局配置 // 构建配置:安装目录默认 + 用户目录覆盖JeecgIntegration 等可由服务器设置写入)
#if DEBUG _configuration = new ConfigurationBuilder()
//TypeAdapterConfig.GlobalSettings.RequireExplicitMapping = true; .SetBasePath(baseDirectory)
#endif .AddJsonFile(Path.Combine("Configuration", "appsettings.json"), optional: false, reloadOnChange: true)
.AddJsonFile(userCfgPath, optional: true, reloadOnChange: true)
.Build();
// 全局配置
TypeAdapterConfig.GlobalSettings.Default
.IgnoreNullValues(true)
.NameMatchingStrategy(NameMatchingStrategy.IgnoreCase);
base.OnStartup(e); // FluentValidation 全局规则级别配置
ValidatorOptions.Global.DefaultRuleLevelCascadeMode = CascadeMode.Stop;
// Mapster 全局配置
#if DEBUG
//TypeAdapterConfig.GlobalSettings.RequireExplicitMapping = true;
#endif
base.OnStartup(e);
}
catch (Exception ex)
{
TryWriteCrashLog("启动阶段异常(配置/框架初始化失败)", ex);
Shutdown(1);
}
}
private static void TryWriteStartupFailure(string message)
{
try
{
var dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "YY.Admin", "logs");
Directory.CreateDirectory(dir);
var file = Path.Combine(dir, $"startup-{DateTime.Now:yyyyMMdd-HHmmss}.txt");
File.WriteAllText(file, message, Encoding.UTF8);
MessageBox.Show($"{message}\n\n已记录{file}", "智能制造MES工控", MessageBoxButton.OK, MessageBoxImage.Error);
}
catch
{
MessageBox.Show(message, "智能制造MES工控", MessageBoxButton.OK, MessageBoxImage.Error);
}
} }
//注册 //注册
protected override void RegisterTypes(IContainerRegistry containerRegistry) protected override void RegisterTypes(IContainerRegistry containerRegistry)

View File

@@ -1,6 +1,7 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using System.IO; using System.IO;
using YY.Admin.Core.Util;
namespace YY.Admin.Helper namespace YY.Admin.Helper
{ {
@@ -11,6 +12,37 @@ namespace YY.Admin.Helper
{ {
private const string DefaultWebSocketPath = "/websocket/scada-sync"; private const string DefaultWebSocketPath = "/websocket/scada-sync";
/// <summary>
/// 安装目录随包发布的默认配置(只读)。
/// </summary>
public static string GetBundledAppSettingsPath()
{
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Configuration", "appsettings.json");
}
/// <summary>
/// 用户覆盖配置(可写),仅覆盖 JeecgIntegration 节点时使用。
/// </summary>
public static string GetUserAppSettingsPath()
{
var dir = AppWritablePaths.EnsureDirectoryExists(AppWritablePaths.ConfigurationDirectory);
return Path.Combine(dir, "appsettings.json");
}
/// <summary>
/// 兼容旧调用:优先返回用于读取的实际路径(存在用户覆盖则用用户文件)。
/// </summary>
public static string GetConfigPath()
{
var user = GetUserAppSettingsPath();
if (File.Exists(user))
{
return user;
}
return GetBundledAppSettingsPath();
}
public class ServerSettingsModel public class ServerSettingsModel
{ {
public string Ip { get; set; } = "127.0.0.1"; public string Ip { get; set; } = "127.0.0.1";
@@ -25,22 +57,35 @@ namespace YY.Admin.Helper
public bool DisconnectConnection { get; set; } = false; public bool DisconnectConnection { get; set; } = false;
} }
public static string GetConfigPath() private static JObject LoadMergedRoot()
{ {
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Configuration", "appsettings.json"); var bundledPath = GetBundledAppSettingsPath();
if (!File.Exists(bundledPath))
{
throw new FileNotFoundException("未找到安装目录默认配置文件 appsettings.json", bundledPath);
}
var root = JObject.Parse(File.ReadAllText(bundledPath));
var userPath = GetUserAppSettingsPath();
if (!File.Exists(userPath))
{
return root;
}
var userRoot = JObject.Parse(File.ReadAllText(userPath));
var userJeecg = userRoot["JeecgIntegration"] as JObject;
if (userJeecg != null)
{
root["JeecgIntegration"] = userJeecg;
}
return root;
} }
public static ServerSettingsModel Load() public static ServerSettingsModel Load()
{ {
var model = new ServerSettingsModel(); var model = new ServerSettingsModel();
var path = GetConfigPath(); var root = LoadMergedRoot();
if (!File.Exists(path))
{
return model;
}
var content = File.ReadAllText(path);
var root = JObject.Parse(content);
var jeecg = root["JeecgIntegration"] as JObject; var jeecg = root["JeecgIntegration"] as JObject;
if (jeecg == null) if (jeecg == null)
{ {
@@ -64,14 +109,7 @@ namespace YY.Admin.Helper
public static void Save(ServerSettingsModel model) public static void Save(ServerSettingsModel model)
{ {
var path = GetConfigPath(); var root = LoadMergedRoot();
if (!File.Exists(path))
{
throw new FileNotFoundException("未找到配置文件 appsettings.json", path);
}
var content = File.ReadAllText(path);
var root = JObject.Parse(content);
var jeecg = root["JeecgIntegration"] as JObject; var jeecg = root["JeecgIntegration"] as JObject;
if (jeecg == null) if (jeecg == null)
{ {
@@ -95,7 +133,9 @@ namespace YY.Admin.Helper
jeecg["WebSocketPath"] = webSocketPath; jeecg["WebSocketPath"] = webSocketPath;
jeecg["DisconnectConnection"] = model.DisconnectConnection; jeecg["DisconnectConnection"] = model.DisconnectConnection;
File.WriteAllText(path, root.ToString(Formatting.Indented)); var userPath = GetUserAppSettingsPath();
var outRoot = new JObject { ["JeecgIntegration"] = jeecg };
File.WriteAllText(userPath, outRoot.ToString(Formatting.Indented));
} }
public static string BuildDefaultWebSocketUrl(string baseScheme, string ip, int port, string basePath, string webSocketPath = DefaultWebSocketPath) public static string BuildDefaultWebSocketUrl(string baseScheme, string ip, int port, string basePath, string webSocketPath = DefaultWebSocketPath)
@@ -106,6 +146,7 @@ namespace YY.Admin.Helper
{ {
safeBasePath = "/" + safeBasePath; safeBasePath = "/" + safeBasePath;
} }
var safeWsPath = NormalizeWebSocketPath(webSocketPath); var safeWsPath = NormalizeWebSocketPath(webSocketPath);
return $"{safeScheme}://{ip}:{port}{safeBasePath}{safeWsPath}"; return $"{safeScheme}://{ip}:{port}{safeBasePath}{safeWsPath}";
} }
@@ -117,6 +158,7 @@ namespace YY.Admin.Helper
{ {
value = "/" + value; value = "/" + value;
} }
return value; return value;
} }
} }

View File

@@ -0,0 +1,23 @@
using System.IO;
using Microsoft.Web.WebView2.Wpf;
using YY.Admin.Core.Util;
namespace YY.Admin.Helper;
/// <summary>
/// WebView2 默认将用户数据目录放在宿主 exe 旁边;安装在 Program Files 时目录只读会导致初始化失败、预览白屏。
/// 统一到当前用户 LocalAppData 下的可写路径。
/// </summary>
public static class WebView2UserDataFolder
{
/// <summary>
/// 为控件创建 CreationProperties须在首次 EnsureCoreWebView2Async 之前赋值)。
/// </summary>
/// <param name="subFolder">子目录名,避免不同场景争用同一 profile。</param>
public static CoreWebView2CreationProperties CreateCreationProperties(string subFolder)
{
var folder = AppWritablePaths.EnsureDirectoryExists(
Path.Combine(AppWritablePaths.LocalApplicationRoot, "WebView2", subFolder));
return new CoreWebView2CreationProperties { UserDataFolder = folder };
}
}

View File

@@ -3,6 +3,7 @@ using System.Text;
using System.Windows; using System.Windows;
using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.Core;
using Microsoft.Web.WebView2.Wpf; using Microsoft.Web.WebView2.Wpf;
using YY.Admin.Core.Util;
namespace YY.Admin.Infrastructure.Print; namespace YY.Admin.Infrastructure.Print;
@@ -42,7 +43,9 @@ public static class HtmlToPdfRenderer
{ {
File.WriteAllText(tempHtml, html, Encoding.UTF8); File.WriteAllText(tempHtml, html, Encoding.UTF8);
var env = await CoreWebView2Environment.CreateAsync(); var userData = AppWritablePaths.EnsureDirectoryExists(
Path.Combine(AppWritablePaths.LocalApplicationRoot, "WebView2", "HtmlToPdf"));
var env = await CoreWebView2Environment.CreateAsync(browserExecutableFolder: null, userDataFolder: userData);
var wv = new WebView2(); var wv = new WebView2();
win = new Window win = new Window

View File

@@ -1,6 +1,6 @@
using System.Windows; using System.Windows;
using System.Windows.Media; using System.Windows.Media;
using Prism.Dialogs;
using YY.Admin.ViewModels.Control; using YY.Admin.ViewModels.Control;
using YY.Admin.ViewModels.Dialogs; using YY.Admin.ViewModels.Dialogs;
using YY.Admin.Views; using YY.Admin.Views;
@@ -20,6 +20,15 @@ using YY.Admin.Views.Print;
namespace YY.Admin namespace YY.Admin
{ {
/// <summary>
/// Prism DialogService 中 <see cref="KnownDialogParameters.WindowName"/> 使用的宿主窗口注册名。
/// </summary>
public static class DialogWindowNames
{
/// <summary>标准边框、可调整大小,且不使用 AllowsTransparency服务器设置等需改 WindowStyle 的对话框)。</summary>
public const string ChromeDialogWindow = "ChromeDialogWindow";
}
public static class NavigationExtensions public static class NavigationExtensions
{ {
/// <summary> /// <summary>
@@ -35,8 +44,9 @@ namespace YY.Admin
containerRegistry.RegisterDialog<ConfirmDialogView, ConfirmDialogViewModel>("ConfirmDialog"); containerRegistry.RegisterDialog<ConfirmDialogView, ConfirmDialogViewModel>("ConfirmDialog");
containerRegistry.RegisterDialog<ServerSettingsDialogView, ServerSettingsDialogViewModel>("ServerSettingsDialog"); containerRegistry.RegisterDialog<ServerSettingsDialogView, ServerSettingsDialogViewModel>("ServerSettingsDialog");
// 设置对话框样式 // 默认透明无边框宿主;需调整 WindowStyle/AllowsTransparency 的对话框改用命名宿主 ChromeDialogWindow
containerRegistry.RegisterDialogWindow<DialogWindow>(); containerRegistry.RegisterDialogWindow<DialogWindow>();
containerRegistry.RegisterDialogWindow<ChromeDialogWindow>(DialogWindowNames.ChromeDialogWindow);
// 注册导航 // 注册导航
containerRegistry.RegisterForNavigation<DashboardView>("DashboardView"); containerRegistry.RegisterForNavigation<DashboardView>("DashboardView");
@@ -103,6 +113,30 @@ namespace YY.Admin
} }
public IDialogResult? Result { get; set; } public IDialogResult? Result { get; set; }
} }
/// <summary>
/// 标准窗口边框宿主:从构造起即 AllowsTransparency=false避免窗口显示后再切换透明属性引发异常。
/// </summary>
public class ChromeDialogWindow : Window, IDialogWindow
{
public ChromeDialogWindow()
{
AllowsTransparency = false;
Background = SystemColors.WindowBrush;
WindowStyle = WindowStyle.SingleBorderWindow;
ResizeMode = ResizeMode.CanResizeWithGrip;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
// 按内容测量客户区高度;勿将 Window.Height 设为与 UserControl 相同数值,
// 否则会与标题栏/边框抢高度导致底部按钮被裁切。
SizeToContent = SizeToContent.WidthAndHeight;
MinWidth = 520;
MinHeight = 360;
Title = "对话框";
}
public IDialogResult? Result { get; set; }
}
//public class DialogWindow : Window, IDialogWindow //public class DialogWindow : Window, IDialogWindow
//{ //{
// public DialogWindow() // public DialogWindow()

View File

@@ -11,6 +11,7 @@ using YY.Admin.Core.EventBus;
using YY.Admin.Core.Helper; using YY.Admin.Core.Helper;
using YY.Admin.Core.Model; using YY.Admin.Core.Model;
using YY.Admin.Core.Session; using YY.Admin.Core.Session;
using YY.Admin.Core.Util;
using HcSkinType = HandyControl.Data.SkinType; using HcSkinType = HandyControl.Data.SkinType;
namespace YY.Admin.ViewModels namespace YY.Admin.ViewModels
@@ -167,7 +168,10 @@ namespace YY.Admin.ViewModels
public static string GetFilePath() public static string GetFilePath()
{ {
string filePathSuffix = string.Format(CommonConst.AppSettingsFilePath, AppSession.CurrentUser!.Account); string filePathSuffix = string.Format(CommonConst.AppSettingsFilePath, AppSession.CurrentUser!.Account);
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, filePathSuffix); var fullPath = Path.Combine(AppWritablePaths.LocalApplicationRoot, filePathSuffix);
var dir = Path.GetDirectoryName(fullPath);
AppWritablePaths.EnsureDirectoryExists(dir!);
return fullPath;
} }
/// <summary> /// <summary>

View File

@@ -2,6 +2,8 @@ using System.Windows;
using System.Windows.Media; using System.Windows.Media;
using System.Net.Http; using System.Net.Http;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Prism.Dialogs;
using YY.Admin;
using YY.Admin.Core.Helper; using YY.Admin.Core.Helper;
using YY.Admin.Core.Session; using YY.Admin.Core.Session;
using YY.Admin.FluentValidation; using YY.Admin.FluentValidation;
@@ -291,7 +293,11 @@ namespace YY.Admin.ViewModels
private void OpenServerSettings() private void OpenServerSettings()
{ {
_dialogService.ShowDialog("ServerSettingsDialog", r => var parameters = new DialogParameters
{
{ KnownDialogParameters.WindowName, DialogWindowNames.ChromeDialogWindow },
};
_dialogService.ShowDialog("ServerSettingsDialog", parameters, r =>
{ {
if (r.Result == ButtonResult.OK) if (r.Result == ButtonResult.OK)
{ {

View File

@@ -1,5 +1,6 @@
using Mapster; using Mapster;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Prism.Dialogs;
using Newtonsoft.Json; using Newtonsoft.Json;
using SqlSugar; using SqlSugar;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
@@ -9,6 +10,7 @@ using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Threading; using System.Windows.Threading;
using YY.Admin;
using YY.Admin.Core; using YY.Admin.Core;
using YY.Admin.Core.Const; using YY.Admin.Core.Const;
using YY.Admin.Core.Model; using YY.Admin.Core.Model;
@@ -16,12 +18,12 @@ using YY.Admin.Core.Services;
using YY.Admin.Core.Session; using YY.Admin.Core.Session;
using YY.Admin.Core.Util; using YY.Admin.Core.Util;
using YY.Admin.Event; using YY.Admin.Event;
using YY.Admin.Helper;
using YY.Admin.Module; using YY.Admin.Module;
using YY.Admin.Services.Service.Auth; using YY.Admin.Services.Service.Auth;
using YY.Admin.Services.Service.Jeecg; using YY.Admin.Services.Service.Jeecg;
using YY.Admin.Services.Service.Menu; using YY.Admin.Services.Service.Menu;
using YY.Admin.ViewModels.Control; using YY.Admin.ViewModels.Control;
using YY.Admin.Helper;
namespace YY.Admin.ViewModels namespace YY.Admin.ViewModels
{ {
@@ -724,7 +726,11 @@ namespace YY.Admin.ViewModels
private void OpenServerSettings() private void OpenServerSettings()
{ {
_dialogService.ShowDialog("ServerSettingsDialog", r => var parameters = new DialogParameters
{
{ KnownDialogParameters.WindowName, DialogWindowNames.ChromeDialogWindow },
};
_dialogService.ShowDialog("ServerSettingsDialog", parameters, r =>
{ {
if (r.Result == ButtonResult.OK) if (r.Result == ButtonResult.OK)
{ {

View File

@@ -2,13 +2,13 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ctls="clr-namespace:YY.Admin.Core.Controls;assembly=YY.Admin.Core" xmlns:ctls="clr-namespace:YY.Admin.Core.Controls;assembly=YY.Admin.Core"
Width="520" Height="420" MinWidth="520">
Loaded="UserControl_Loaded">
<Border Background="White" BorderBrush="#f0f0f0" BorderThickness="1" CornerRadius="4"> <Border Background="White" BorderBrush="#f0f0f0" BorderThickness="1" CornerRadius="4">
<Grid Margin="0"> <Grid Margin="0">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="*"/> <!-- Auto按表单实际高度排列避免固定窗体高度时中间行被压扁裁切 -->
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>

View File

@@ -1,5 +1,4 @@
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows;
namespace YY.Admin.Views.Dialogs namespace YY.Admin.Views.Dialogs
{ {
@@ -12,22 +11,5 @@ namespace YY.Admin.Views.Dialogs
{ {
InitializeComponent(); InitializeComponent();
} }
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
var win = Window.GetWindow(this);
if (win == null) return;
// 必须先关闭 AllowsTransparency再改 WindowStyle否则会抛出
// 「当 AllowsTransparency 为 true 时WindowStyle.None 是唯一有效值」)
win.AllowsTransparency = false;
win.WindowStyle = WindowStyle.SingleBorderWindow;
win.ResizeMode = ResizeMode.CanResizeWithGrip;
win.SizeToContent = SizeToContent.Manual;
win.MinWidth = 520;
win.MinHeight = 420;
if (win.Width < 520) win.Width = 520;
if (win.Height < 420) win.Height = 420;
}
} }
} }

View File

@@ -6,6 +6,7 @@ using System.Text.RegularExpressions;
using System.Windows; using System.Windows;
using YY.Admin.Core.Entity; using YY.Admin.Core.Entity;
using YY.Admin.Core.Services; using YY.Admin.Core.Services;
using YY.Admin.Helper;
using YY.Admin.Services.Service.Print; using YY.Admin.Services.Service.Print;
namespace YY.Admin.Views.Print; namespace YY.Admin.Views.Print;
@@ -32,6 +33,8 @@ public partial class PrintPreviewWindow : HandyControl.Controls.Window
_printDotService = printDotService; _printDotService = printDotService;
_initialPrinterName = selectedPrinterName; _initialPrinterName = selectedPrinterName;
WebView.CreationProperties = WebView2UserDataFolder.CreateCreationProperties("PrintPreview");
TbTemplateName.Text = template.TemplateName ?? "(未命名)"; TbTemplateName.Text = template.TemplateName ?? "(未命名)";
TbTemplateCode.Text = $"编码:{template.TemplateCode} " + TbTemplateCode.Text = $"编码:{template.TemplateCode} " +
$"尺寸:{template.PaperWidthMm ?? 210}×{template.PaperHeightMm ?? 297} mm " + $"尺寸:{template.PaperWidthMm ?? 210}×{template.PaperHeightMm ?? 297} mm " +

View File

@@ -10,6 +10,7 @@ using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Controls.Primitives; using System.Windows.Controls.Primitives;
using YY.Admin.Core.Services; using YY.Admin.Core.Services;
using YY.Admin.Helper;
using YY.Admin.ViewModels.RawMaterialEntry; using YY.Admin.ViewModels.RawMaterialEntry;
namespace YY.Admin.Views.RawMaterialEntry; namespace YY.Admin.Views.RawMaterialEntry;
@@ -101,6 +102,7 @@ public partial class RawMaterialCardGenerateConfirmWindow : HandyControl.Control
Func<RawMaterialCardGeneratePlanRow, string> previewHtmlBuilder) Func<RawMaterialCardGeneratePlanRow, string> previewHtmlBuilder)
{ {
InitializeComponent(); InitializeComponent();
PreviewWebView.CreationProperties = WebView2UserDataFolder.CreateCreationProperties("RawMaterialCardConfirm");
_printDotService = printDotService; _printDotService = printDotService;
_previewHtmlBuilder = previewHtmlBuilder; _previewHtmlBuilder = previewHtmlBuilder;
HeaderText = $"共 {planItems.Count} 张,左侧展示即将生成的原材料卡片,右侧展示业务关联打印模板预览。"; HeaderText = $"共 {planItems.Count} 张,左侧展示即将生成的原材料卡片,右侧展示业务关联打印模板预览。";

View File

@@ -2,6 +2,7 @@ using System.ComponentModel;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Controls.Primitives; using System.Windows.Controls.Primitives;
using YY.Admin.Helper;
using YY.Admin.ViewModels.RawMaterialEntry; using YY.Admin.ViewModels.RawMaterialEntry;
namespace YY.Admin.Views.RawMaterialEntry; namespace YY.Admin.Views.RawMaterialEntry;
@@ -14,6 +15,7 @@ public partial class RawMaterialEntryOperationView : UserControl
public RawMaterialEntryOperationView() public RawMaterialEntryOperationView()
{ {
InitializeComponent(); InitializeComponent();
PrintPreviewWebView.CreationProperties = WebView2UserDataFolder.CreateCreationProperties("RawMaterialEntryPreview");
Loaded += OnLoaded; Loaded += OnLoaded;
DataContextChanged += OnDataContextChanged; DataContextChanged += OnDataContextChanged;
Unloaded += (_, _) => DetachVm(); Unloaded += (_, _) => DetachVm();

View File

@@ -59,6 +59,8 @@
<ItemGroup> <ItemGroup>
<Reference Include="HandyControl"> <Reference Include="HandyControl">
<HintPath>..\YY.Admin.Core\libs\HandyControl.dll</HintPath> <HintPath>..\YY.Admin.Core\libs\HandyControl.dll</HintPath>
<!-- 单文件宿主下外部 UI 库需外置,否则易出现 pack:// 资源加载失败(启动即退出) -->
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Reference> </Reference>
</ItemGroup> </ItemGroup>
@@ -106,10 +108,12 @@
</None> </None>
<None Update="Updates\version.xml"> <None Update="Updates\version.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<!-- 单文件发布时须外置,否则安装目录无物理文件 -->
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</None> </None>
</ItemGroup> </ItemGroup>
<!-- 主程序 BaseDirectory 需含 Configuration/appsettings.json;带 RuntimeIdentifier 时子项目 None 未必进入主输出 --> <!-- 主程序须携带 Configuration/appsettings.json。.NET 单文件发布时 Linked None 的 ExcludeFromSingleFile 可能仍不落盘,故 Publish 后再强制复制一次 -->
<ItemGroup> <ItemGroup>
<None Include="..\YY.Admin.Services\Configuration\appsettings.json"> <None Include="..\YY.Admin.Services\Configuration\appsettings.json">
<Link>Configuration\appsettings.json</Link> <Link>Configuration\appsettings.json</Link>
@@ -117,4 +121,13 @@
</None> </None>
</ItemGroup> </ItemGroup>
<Target Name="EnsureAppSettingsJsonInPublishDirectory" AfterTargets="Publish">
<PropertyGroup>
<_AppSettingsSource>$(MSBuildProjectDirectory)\..\YY.Admin.Services\Configuration\appsettings.json</_AppSettingsSource>
</PropertyGroup>
<Error Condition="!Exists('$(_AppSettingsSource)')" Text="缺少源文件 $(_AppSettingsSource),无法发布 Configuration\appsettings.json。" />
<MakeDir Directories="$(PublishDir)Configuration" />
<Copy SourceFiles="$(_AppSettingsSource)" DestinationFiles="$(PublishDir)Configuration\appsettings.json" SkipUnchangedFiles="false" />
</Target>
</Project> </Project>

View File

@@ -0,0 +1,78 @@
; Inno Setup 6 安装脚本 — 用法见同目录 build-installer.ps1
; 需先安装: https://jrsoftware.org/isdl.php
#define MyAppName "智能制造MES工控"
#define MyAppExeName "YY.Admin.exe"
#define MyAppVersion "1.1.0"
#define MyPublisher "星数连科技科技有限公司"
; 相对本 .iss 文件位置installer\ 下的上一级为 yy-admin-master
#define PublishRoot "..\YY.Admin\bin\Release\net8.0-windows10.0.19041\win-x64\publish"
[Setup]
AppId={{B7E8F4A2-9C1D-4E6F-8A3B-2D5E9C1F4A7B}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyPublisher}
AppPublisherURL=https://www.example.com/
DefaultDirName={autopf}\{#MyPublisher}\{#MyAppName}
DefaultGroupName={#MyAppName}
AllowNoIcons=yes
OutputDir=..\_installer_output
OutputBaseFilename=YY.Admin_Setup_{#MyAppVersion}_win-x64
Compression=lzma2/max
SolidCompression=yes
WizardStyle=modern
PrivilegesRequired=admin
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
MinVersion=10.0.17763
DisableProgramGroupPage=no
DisableDirPage=no
UninstallDisplayIcon={app}\{#MyAppExeName}
SetupLogging=yes
; 向导语言:使用 Inno 自带的 Default.isl英文。若本机存在 Languages\ChineseSimplified.isl
; 可改为: Name: "chinesesimp"; MessagesFile: "compiler:Languages\ChineseSimplified.isl"
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "desktopicon"; Description: "在桌面创建快捷方式"; GroupDescription: "附加图标:"; Flags: unchecked
Name: "installwebview2"; Description: "安装 Microsoft Edge WebView2 运行时(内嵌网页需要;若已安装会自动跳过)"; GroupDescription: "运行环境:"; Flags: checkedonce
[Files]
; 发布目录完整拷贝(含 Configuration、Updates 等)
Source: "{#PublishRoot}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
; WebView2 引导安装包(由 build-installer.ps1 下载到 redist编译时若无文件可加 Flags skipifsourcedoesntexist
Source: "redist\MicrosoftEdgeWebview2Setup.exe"; DestDir: "{tmp}"; Flags: deleteafterinstall skipifsourcedoesntexist
[Icons]
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; WorkingDir: "{app}"
Name: "{group}\卸载 {#MyAppName}"; Filename: "{uninstallexe}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; WorkingDir: "{app}"; Tasks: desktopicon
[Run]
Filename: "{tmp}\MicrosoftEdgeWebview2Setup.exe"; Parameters: "/silent /install"; StatusMsg: "正在安装 WebView2 运行时..."; Flags: waituntilterminated; Tasks: installwebview2; Check: ShouldInstallWebView2()
Filename: "{app}\{#MyAppExeName}"; Description: "启动 {#MyAppName}"; Flags: nowait postinstall skipifsilent
[Code]
// 安装包是否携带了 WebView2 引导程序(已释放到临时目录)
function WebView2BootstrapInTmp: Boolean;
begin
Result := FileExists(ExpandConstant('{tmp}\MicrosoftEdgeWebview2Setup.exe'));
end;
// Evergreen WebView2 是否存在(注册表中存在版本号则认为已装)
function WebView2RuntimeInstalled: Boolean;
var
Ver: String;
begin
Result := RegQueryStringValue(HKLM, 'SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}', 'pv', Ver) and (Trim(Ver) <> '');
end;
function ShouldInstallWebView2: Boolean;
begin
Result := WebView2BootstrapInTmp and (not WebView2RuntimeInstalled);
end;

View File

@@ -0,0 +1,89 @@
# Release publish + WebView2 bootstrap download + Inno Setup ISCC
# Requires Inno Setup 6 (ISCC.exe)
$ErrorActionPreference = 'Stop'
$Root = Split-Path -Parent $PSScriptRoot
$Csproj = Join-Path $Root 'YY.Admin\YY.Admin.csproj'
$PublishRel = 'YY.Admin\bin\Release\net8.0-windows10.0.19041\win-x64\publish'
$PublishDir = Join-Path $Root $PublishRel
$RedistDir = Join-Path $PSScriptRoot 'redist'
$WebView2Exe = Join-Path $RedistDir 'MicrosoftEdgeWebview2Setup.exe'
$WebView2Url = 'https://go.microsoft.com/fwlink/p/?LinkId=2124703'
Write-Host '>>> dotnet publish (Release, win-x64)...'
dotnet publish $Csproj -c Release `
-p:IncludeNativeLibrariesForSelfExtract=true `
-p:EnableCompressionInSingleFile=true `
--verbosity minimal
if (-not (Test-Path $PublishDir)) {
throw "Publish output not found: $PublishDir"
}
$cfgPublish = Join-Path $PublishDir 'Configuration\appsettings.json'
if (-not (Test-Path $cfgPublish)) {
throw "Missing Configuration\appsettings.json under publish (ExcludeFromSingleFile required). Path: $cfgPublish"
}
New-Item -ItemType Directory -Force -Path $RedistDir | Out-Null
if (-not (Test-Path $WebView2Exe)) {
Write-Host '>>> Downloading WebView2 Evergreen bootstrapper...'
Invoke-WebRequest -Uri $WebView2Url -OutFile $WebView2Exe -UseBasicParsing
}
function Find-InnoCompiler {
if ($env:ISCC -and (Test-Path -LiteralPath $env:ISCC)) {
return $env:ISCC
}
if ($env:INNO_SETUP_DIR) {
$p = Join-Path $env:INNO_SETUP_DIR.TrimEnd('\') 'ISCC.exe'
if (Test-Path -LiteralPath $p) {
return $p
}
}
$cmd = Get-Command 'iscc.exe' -ErrorAction SilentlyContinue
if ($cmd -and $cmd.Source -and (Test-Path -LiteralPath $cmd.Source)) {
return $cmd.Source
}
$candidates = @(
'D:\Inno Setup 6\ISCC.exe'
"${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe"
"${env:ProgramFiles}\Inno Setup 6\ISCC.exe"
"${env:LocalAppData}\Programs\Inno Setup 6\ISCC.exe"
)
foreach ($c in $candidates) {
if ($c -and (Test-Path -LiteralPath $c)) {
return $c
}
}
return $null
}
$iscc = Find-InnoCompiler
if (-not $iscc) {
throw @'
ISCC.exe not found.
1) Install Inno Setup 6: https://jrsoftware.org/isdl.php
2) Or set env ISCC to full path of ISCC.exe, e.g.:
set ISCC=C:\Program Files (x86)\Inno Setup 6\ISCC.exe
3) Or set INNO_SETUP_DIR to the Inno Setup 6 folder.
Then run this script again.
'@
}
Write-Host ">>> Using ISCC: $iscc"
$iss = Join-Path $PSScriptRoot 'YY.Admin.Setup.iss'
Write-Host '>>> Compiling installer (working dir: installer)...'
Push-Location $PSScriptRoot
try {
& $iscc "`"$iss`""
} finally {
Pop-Location
}
$outDir = Join-Path $Root '_installer_output'
Write-Host ">>> Done. Output folder: $outDir"

View File

@@ -0,0 +1,2 @@
# WebView2 Evergreen 引导程序由 build-installer.ps1 自动下载,不必提交仓库
MicrosoftEdgeWebview2Setup.exe