diff --git a/yy-admin-master/YY.Admin-Release-win-x64-publish.zip b/yy-admin-master/YY.Admin-Release-win-x64-publish.zip new file mode 100644 index 0000000..998d676 Binary files /dev/null and b/yy-admin-master/YY.Admin-Release-win-x64-publish.zip differ diff --git a/yy-admin-master/YY.Admin.Core/SqlSugar/SqlSugarSetup.cs b/yy-admin-master/YY.Admin.Core/SqlSugar/SqlSugarSetup.cs index 94ab22f..5a9df0c 100644 --- a/yy-admin-master/YY.Admin.Core/SqlSugar/SqlSugarSetup.cs +++ b/yy-admin-master/YY.Admin.Core/SqlSugar/SqlSugarSetup.cs @@ -10,6 +10,7 @@ using System.Collections; using System.Collections.Generic; using System.Data; using System.Diagnostics; +using System.IO; using System.Linq; using System.Linq.Dynamic.Core; using System.Reflection; @@ -19,7 +20,9 @@ using Yitter.IdGenerator; using YY.Admin.Core; using YY.Admin.Core.Extension; using YY.Admin.Core.Option; +using YY.Admin.Core.SeedData; using YY.Admin.Core.Session; +using YY.Admin.Core.Util; using DbType = SqlSugar.DbType; namespace YY.Admin.Core.SqlSugar @@ -160,6 +163,12 @@ namespace YY.Admin.Core.SqlSugar 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 { EntityNameService = (type, entity) => // 处理表 @@ -250,6 +259,96 @@ namespace YY.Admin.Core.SqlSugar //if (config.DbSettings.EnableInitView) InitView(dbProvider); // 初始化种子数据 if (config.SeedSettings.EnableInitSeed) InitSeedData(db, config); + // 关闭全量种子时首启可能无菜单数据;补一份基准菜单,避免打包版本左侧空白 + EnsureBaselineSysMenuSeed(db, config); + } + + /// + /// 将 SQLite 连接串中的相对 DataSource 解析为基于应用程序基目录的绝对路径。 + /// + 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; + } + } + + /// + /// 未启用 EnableInitSeed 且 sys_menu 为空时,写入 SysMenu 种子,避免发布后界面无功能菜单。 + /// + 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().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 + { + // 启动阶段不因兜底种子失败而阻断 + } } /// diff --git a/yy-admin-master/YY.Admin.Core/Util/AppWritablePaths.cs b/yy-admin-master/YY.Admin.Core/Util/AppWritablePaths.cs new file mode 100644 index 0000000..cd5abca --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Util/AppWritablePaths.cs @@ -0,0 +1,38 @@ +using System.IO; + +namespace YY.Admin.Core.Util; + +/// +/// 当前用户可写应用数据目录(避免安装在 Program Files 时无写权限)。 +/// +public static class AppWritablePaths +{ + private static readonly string Root = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "YY.Admin"); + + /// 应用私有根目录:%LocalAppData%\YY.Admin + public static string LocalApplicationRoot => Root; + + /// SQLite 等业务数据库目录。 + public static string DataDirectory => Path.Combine(Root, "Data"); + + /// 用户覆盖的配置、Jeecg 同步状态等。 + public static string ConfigurationDirectory => Path.Combine(Root, "Configuration"); + + /// 按账号划分的本地设置(对应 CommonConst.AppSettingsFilePath 前缀)。 + public static string AccountSettingsRootDirectory => Path.Combine(Root, "AppSettings"); + + /// + /// 创建目录(若不存在)并返回路径。 + /// + public static string EnsureDirectoryExists(string directoryPath) + { + if (!string.IsNullOrWhiteSpace(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } + + return directoryPath; + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/Jeecg/JeecgLoginLogReportService.cs b/yy-admin-master/YY.Admin.Services/Service/Jeecg/JeecgLoginLogReportService.cs index b9b94f3..03f4e24 100644 --- a/yy-admin-master/YY.Admin.Services/Service/Jeecg/JeecgLoginLogReportService.cs +++ b/yy-admin-master/YY.Admin.Services/Service/Jeecg/JeecgLoginLogReportService.cs @@ -3,6 +3,7 @@ using System.Net.Http; using System.IO; using System.Text; using System.Text.Json; +using YY.Admin.Core.Util; namespace YY.Admin.Services.Service.Jeecg; @@ -226,7 +227,8 @@ public class JeecgLoginLogReportService : IJeecgLoginLogReportService, IClientLo 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) diff --git a/yy-admin-master/YY.Admin.Services/Service/Jeecg/JeecgSyncStateStore.cs b/yy-admin-master/YY.Admin.Services/Service/Jeecg/JeecgSyncStateStore.cs index abedaf7..6b34859 100644 --- a/yy-admin-master/YY.Admin.Services/Service/Jeecg/JeecgSyncStateStore.cs +++ b/yy-admin-master/YY.Admin.Services/Service/Jeecg/JeecgSyncStateStore.cs @@ -1,10 +1,11 @@ using System.IO; using System.Text.Json; +using YY.Admin.Core.Util; namespace YY.Admin.Services.Service.Jeecg { /// - /// 读写本地 Jeecg 同步状态文件(与 appsettings 同目录下的 Configuration) + /// 读写本地 Jeecg 同步状态文件(用户可写目录,避免 Program Files 无写权限)。 /// public class JeecgSyncStateStore { @@ -12,7 +13,7 @@ namespace YY.Admin.Services.Service.Jeecg public JeecgSyncStateStore() { - var dir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Configuration"); + var dir = AppWritablePaths.EnsureDirectoryExists(AppWritablePaths.ConfigurationDirectory); _filePath = Path.Combine(dir, "jeecg-sync-state.json"); } diff --git a/yy-admin-master/YY.Admin/App.xaml.cs b/yy-admin-master/YY.Admin/App.xaml.cs index 2345a90..f6cc0cf 100644 --- a/yy-admin-master/YY.Admin/App.xaml.cs +++ b/yy-admin-master/YY.Admin/App.xaml.cs @@ -4,7 +4,9 @@ using Mapster; using Microsoft.Extensions.Configuration; using NewLife; using System.IO; +using System.Text; using System.Windows; +using System.Windows.Threading; using YY.Admin.Core; using YY.Admin.EventBus; using YY.Admin.Filter; @@ -14,6 +16,7 @@ using YY.Admin.Services.Service.Print; using YY.Admin.Setup; using YY.Admin.ViewModels; using YY.Admin.Views; +using YY.Admin.Core.Util; namespace YY.Admin { /// @@ -24,32 +27,116 @@ namespace YY.Admin private IConfiguration? _configuration; private ILoggerService? _logger; 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() { return Container.Resolve(); } protected override void OnStartup(StartupEventArgs e) { - var baseDirectory = AppDomain.CurrentDomain.BaseDirectory; - // 构建配置 - _configuration = new ConfigurationBuilder() - .SetBasePath(baseDirectory) - .AddJsonFile("Configuration/appsettings.json", optional: false, reloadOnChange: true) - .Build(); - // 全局配置 - TypeAdapterConfig.GlobalSettings.Default - .IgnoreNullValues(true) - .NameMatchingStrategy(NameMatchingStrategy.IgnoreCase); + try + { + var baseDirectory = AppDomain.CurrentDomain.BaseDirectory; + var cfgPath = Path.Combine(baseDirectory, "Configuration", "appsettings.json"); + if (!File.Exists(cfgPath)) + { + var msg = $"未找到配置文件(请确认与 YY.Admin.exe 同目录存在 Configuration\\appsettings.json):\n{cfgPath}"; + TryWriteStartupFailure(msg); + Shutdown(1); + return; + } - // FluentValidation 全局规则级别配置 - ValidatorOptions.Global.DefaultRuleLevelCascadeMode = CascadeMode.Stop; + var userCfgPath = Path.Combine( + AppWritablePaths.EnsureDirectoryExists(AppWritablePaths.ConfigurationDirectory), + "appsettings.json"); - // Mapster 全局配置 - #if DEBUG - //TypeAdapterConfig.GlobalSettings.RequireExplicitMapping = true; - #endif + // 构建配置:安装目录默认 + 用户目录覆盖(JeecgIntegration 等可由服务器设置写入) + _configuration = new ConfigurationBuilder() + .SetBasePath(baseDirectory) + .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) diff --git a/yy-admin-master/YY.Admin/Helper/ServerSettingsStore.cs b/yy-admin-master/YY.Admin/Helper/ServerSettingsStore.cs index aace8dd..acb7c10 100644 --- a/yy-admin-master/YY.Admin/Helper/ServerSettingsStore.cs +++ b/yy-admin-master/YY.Admin/Helper/ServerSettingsStore.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System.IO; +using YY.Admin.Core.Util; namespace YY.Admin.Helper { @@ -11,6 +12,37 @@ namespace YY.Admin.Helper { private const string DefaultWebSocketPath = "/websocket/scada-sync"; + /// + /// 安装目录随包发布的默认配置(只读)。 + /// + public static string GetBundledAppSettingsPath() + { + return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Configuration", "appsettings.json"); + } + + /// + /// 用户覆盖配置(可写),仅覆盖 JeecgIntegration 节点时使用。 + /// + public static string GetUserAppSettingsPath() + { + var dir = AppWritablePaths.EnsureDirectoryExists(AppWritablePaths.ConfigurationDirectory); + return Path.Combine(dir, "appsettings.json"); + } + + /// + /// 兼容旧调用:优先返回用于读取的实际路径(存在用户覆盖则用用户文件)。 + /// + public static string GetConfigPath() + { + var user = GetUserAppSettingsPath(); + if (File.Exists(user)) + { + return user; + } + + return GetBundledAppSettingsPath(); + } + public class ServerSettingsModel { public string Ip { get; set; } = "127.0.0.1"; @@ -25,22 +57,35 @@ namespace YY.Admin.Helper 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() { var model = new ServerSettingsModel(); - var path = GetConfigPath(); - if (!File.Exists(path)) - { - return model; - } - - var content = File.ReadAllText(path); - var root = JObject.Parse(content); + var root = LoadMergedRoot(); var jeecg = root["JeecgIntegration"] as JObject; if (jeecg == null) { @@ -64,14 +109,7 @@ namespace YY.Admin.Helper public static void Save(ServerSettingsModel model) { - var path = GetConfigPath(); - if (!File.Exists(path)) - { - throw new FileNotFoundException("未找到配置文件 appsettings.json", path); - } - - var content = File.ReadAllText(path); - var root = JObject.Parse(content); + var root = LoadMergedRoot(); var jeecg = root["JeecgIntegration"] as JObject; if (jeecg == null) { @@ -95,7 +133,9 @@ namespace YY.Admin.Helper jeecg["WebSocketPath"] = webSocketPath; 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) @@ -106,6 +146,7 @@ namespace YY.Admin.Helper { safeBasePath = "/" + safeBasePath; } + var safeWsPath = NormalizeWebSocketPath(webSocketPath); return $"{safeScheme}://{ip}:{port}{safeBasePath}{safeWsPath}"; } @@ -117,6 +158,7 @@ namespace YY.Admin.Helper { value = "/" + value; } + return value; } } diff --git a/yy-admin-master/YY.Admin/Helper/WebView2UserDataFolder.cs b/yy-admin-master/YY.Admin/Helper/WebView2UserDataFolder.cs new file mode 100644 index 0000000..0b8a423 --- /dev/null +++ b/yy-admin-master/YY.Admin/Helper/WebView2UserDataFolder.cs @@ -0,0 +1,23 @@ +using System.IO; +using Microsoft.Web.WebView2.Wpf; +using YY.Admin.Core.Util; + +namespace YY.Admin.Helper; + +/// +/// WebView2 默认将用户数据目录放在宿主 exe 旁边;安装在 Program Files 时目录只读会导致初始化失败、预览白屏。 +/// 统一到当前用户 LocalAppData 下的可写路径。 +/// +public static class WebView2UserDataFolder +{ + /// + /// 为控件创建 CreationProperties(须在首次 EnsureCoreWebView2Async 之前赋值)。 + /// + /// 子目录名,避免不同场景争用同一 profile。 + public static CoreWebView2CreationProperties CreateCreationProperties(string subFolder) + { + var folder = AppWritablePaths.EnsureDirectoryExists( + Path.Combine(AppWritablePaths.LocalApplicationRoot, "WebView2", subFolder)); + return new CoreWebView2CreationProperties { UserDataFolder = folder }; + } +} diff --git a/yy-admin-master/YY.Admin/Infrastructure/Print/HtmlToPdfRenderer.cs b/yy-admin-master/YY.Admin/Infrastructure/Print/HtmlToPdfRenderer.cs index d2edf37..25cd11c 100644 --- a/yy-admin-master/YY.Admin/Infrastructure/Print/HtmlToPdfRenderer.cs +++ b/yy-admin-master/YY.Admin/Infrastructure/Print/HtmlToPdfRenderer.cs @@ -3,6 +3,7 @@ using System.Text; using System.Windows; using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.Wpf; +using YY.Admin.Core.Util; namespace YY.Admin.Infrastructure.Print; @@ -42,7 +43,9 @@ public static class HtmlToPdfRenderer { 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(); win = new Window diff --git a/yy-admin-master/YY.Admin/Module/NavigationExtensions.cs b/yy-admin-master/YY.Admin/Module/NavigationExtensions.cs index 540358c..121fcbf 100644 --- a/yy-admin-master/YY.Admin/Module/NavigationExtensions.cs +++ b/yy-admin-master/YY.Admin/Module/NavigationExtensions.cs @@ -1,6 +1,6 @@ - using System.Windows; using System.Windows.Media; +using Prism.Dialogs; using YY.Admin.ViewModels.Control; using YY.Admin.ViewModels.Dialogs; using YY.Admin.Views; @@ -20,6 +20,15 @@ using YY.Admin.Views.Print; namespace YY.Admin { + /// + /// Prism DialogService 中 使用的宿主窗口注册名。 + /// + public static class DialogWindowNames + { + /// 标准边框、可调整大小,且不使用 AllowsTransparency(服务器设置等需改 WindowStyle 的对话框)。 + public const string ChromeDialogWindow = "ChromeDialogWindow"; + } + public static class NavigationExtensions { /// @@ -35,8 +44,9 @@ namespace YY.Admin containerRegistry.RegisterDialog("ConfirmDialog"); containerRegistry.RegisterDialog("ServerSettingsDialog"); - // 设置对话框样式 + // 默认透明无边框宿主;需调整 WindowStyle/AllowsTransparency 的对话框改用命名宿主 ChromeDialogWindow containerRegistry.RegisterDialogWindow(); + containerRegistry.RegisterDialogWindow(DialogWindowNames.ChromeDialogWindow); // 注册导航 containerRegistry.RegisterForNavigation("DashboardView"); @@ -103,6 +113,30 @@ namespace YY.Admin } public IDialogResult? Result { get; set; } } + + /// + /// 标准窗口边框宿主:从构造起即 AllowsTransparency=false,避免窗口显示后再切换透明属性引发异常。 + /// + 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 DialogWindow() diff --git a/yy-admin-master/YY.Admin/ViewModels/AppSettingsViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/AppSettingsViewModel.cs index 39c097a..1b852b9 100644 --- a/yy-admin-master/YY.Admin/ViewModels/AppSettingsViewModel.cs +++ b/yy-admin-master/YY.Admin/ViewModels/AppSettingsViewModel.cs @@ -11,6 +11,7 @@ using YY.Admin.Core.EventBus; using YY.Admin.Core.Helper; using YY.Admin.Core.Model; using YY.Admin.Core.Session; +using YY.Admin.Core.Util; using HcSkinType = HandyControl.Data.SkinType; namespace YY.Admin.ViewModels @@ -167,7 +168,10 @@ namespace YY.Admin.ViewModels public static string GetFilePath() { 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; } /// diff --git a/yy-admin-master/YY.Admin/ViewModels/LoginWindowViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/LoginWindowViewModel.cs index 2ae9053..7d422ba 100644 --- a/yy-admin-master/YY.Admin/ViewModels/LoginWindowViewModel.cs +++ b/yy-admin-master/YY.Admin/ViewModels/LoginWindowViewModel.cs @@ -2,6 +2,8 @@ using System.Windows; using System.Windows.Media; using System.Net.Http; using Microsoft.Extensions.Configuration; +using Prism.Dialogs; +using YY.Admin; using YY.Admin.Core.Helper; using YY.Admin.Core.Session; using YY.Admin.FluentValidation; @@ -291,7 +293,11 @@ namespace YY.Admin.ViewModels 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) { diff --git a/yy-admin-master/YY.Admin/ViewModels/MainWindowViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/MainWindowViewModel.cs index 73c958d..f337fba 100644 --- a/yy-admin-master/YY.Admin/ViewModels/MainWindowViewModel.cs +++ b/yy-admin-master/YY.Admin/ViewModels/MainWindowViewModel.cs @@ -1,5 +1,6 @@ using Mapster; using Microsoft.Extensions.Configuration; +using Prism.Dialogs; using Newtonsoft.Json; using SqlSugar; using System.Collections.ObjectModel; @@ -9,6 +10,7 @@ using System.Windows; using System.Windows.Input; using System.Windows.Media; using System.Windows.Threading; +using YY.Admin; using YY.Admin.Core; using YY.Admin.Core.Const; using YY.Admin.Core.Model; @@ -16,12 +18,12 @@ using YY.Admin.Core.Services; using YY.Admin.Core.Session; using YY.Admin.Core.Util; using YY.Admin.Event; +using YY.Admin.Helper; using YY.Admin.Module; using YY.Admin.Services.Service.Auth; using YY.Admin.Services.Service.Jeecg; using YY.Admin.Services.Service.Menu; using YY.Admin.ViewModels.Control; -using YY.Admin.Helper; namespace YY.Admin.ViewModels { @@ -724,7 +726,11 @@ namespace YY.Admin.ViewModels 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) { diff --git a/yy-admin-master/YY.Admin/Views/Dialogs/ServerSettingsDialogView.xaml b/yy-admin-master/YY.Admin/Views/Dialogs/ServerSettingsDialogView.xaml index c2c16f4..2f2bc05 100644 --- a/yy-admin-master/YY.Admin/Views/Dialogs/ServerSettingsDialogView.xaml +++ b/yy-admin-master/YY.Admin/Views/Dialogs/ServerSettingsDialogView.xaml @@ -2,13 +2,13 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ctls="clr-namespace:YY.Admin.Core.Controls;assembly=YY.Admin.Core" - Width="520" Height="420" - Loaded="UserControl_Loaded"> + MinWidth="520"> - + + diff --git a/yy-admin-master/YY.Admin/Views/Dialogs/ServerSettingsDialogView.xaml.cs b/yy-admin-master/YY.Admin/Views/Dialogs/ServerSettingsDialogView.xaml.cs index 481dd67..d5e2b3d 100644 --- a/yy-admin-master/YY.Admin/Views/Dialogs/ServerSettingsDialogView.xaml.cs +++ b/yy-admin-master/YY.Admin/Views/Dialogs/ServerSettingsDialogView.xaml.cs @@ -1,5 +1,4 @@ using System.Windows.Controls; -using System.Windows; namespace YY.Admin.Views.Dialogs { @@ -12,22 +11,5 @@ namespace YY.Admin.Views.Dialogs { 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; - } } } diff --git a/yy-admin-master/YY.Admin/Views/Print/PrintPreviewWindow.xaml.cs b/yy-admin-master/YY.Admin/Views/Print/PrintPreviewWindow.xaml.cs index cfbe240..3000868 100644 --- a/yy-admin-master/YY.Admin/Views/Print/PrintPreviewWindow.xaml.cs +++ b/yy-admin-master/YY.Admin/Views/Print/PrintPreviewWindow.xaml.cs @@ -6,6 +6,7 @@ using System.Text.RegularExpressions; using System.Windows; using YY.Admin.Core.Entity; using YY.Admin.Core.Services; +using YY.Admin.Helper; using YY.Admin.Services.Service.Print; namespace YY.Admin.Views.Print; @@ -32,6 +33,8 @@ public partial class PrintPreviewWindow : HandyControl.Controls.Window _printDotService = printDotService; _initialPrinterName = selectedPrinterName; + WebView.CreationProperties = WebView2UserDataFolder.CreateCreationProperties("PrintPreview"); + TbTemplateName.Text = template.TemplateName ?? "(未命名)"; TbTemplateCode.Text = $"编码:{template.TemplateCode} " + $"尺寸:{template.PaperWidthMm ?? 210}×{template.PaperHeightMm ?? 297} mm " + diff --git a/yy-admin-master/YY.Admin/Views/RawMaterialEntry/RawMaterialCardGenerateConfirmWindow.xaml.cs b/yy-admin-master/YY.Admin/Views/RawMaterialEntry/RawMaterialCardGenerateConfirmWindow.xaml.cs index 5c02d67..bcab0a9 100644 --- a/yy-admin-master/YY.Admin/Views/RawMaterialEntry/RawMaterialCardGenerateConfirmWindow.xaml.cs +++ b/yy-admin-master/YY.Admin/Views/RawMaterialEntry/RawMaterialCardGenerateConfirmWindow.xaml.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using System.Windows; using System.Windows.Controls.Primitives; using YY.Admin.Core.Services; +using YY.Admin.Helper; using YY.Admin.ViewModels.RawMaterialEntry; namespace YY.Admin.Views.RawMaterialEntry; @@ -101,6 +102,7 @@ public partial class RawMaterialCardGenerateConfirmWindow : HandyControl.Control Func previewHtmlBuilder) { InitializeComponent(); + PreviewWebView.CreationProperties = WebView2UserDataFolder.CreateCreationProperties("RawMaterialCardConfirm"); _printDotService = printDotService; _previewHtmlBuilder = previewHtmlBuilder; HeaderText = $"共 {planItems.Count} 张,左侧展示即将生成的原材料卡片,右侧展示业务关联打印模板预览。"; diff --git a/yy-admin-master/YY.Admin/Views/RawMaterialEntry/RawMaterialEntryOperationView.xaml.cs b/yy-admin-master/YY.Admin/Views/RawMaterialEntry/RawMaterialEntryOperationView.xaml.cs index c0e2aac..fbfbf0e 100644 --- a/yy-admin-master/YY.Admin/Views/RawMaterialEntry/RawMaterialEntryOperationView.xaml.cs +++ b/yy-admin-master/YY.Admin/Views/RawMaterialEntry/RawMaterialEntryOperationView.xaml.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; +using YY.Admin.Helper; using YY.Admin.ViewModels.RawMaterialEntry; namespace YY.Admin.Views.RawMaterialEntry; @@ -14,6 +15,7 @@ public partial class RawMaterialEntryOperationView : UserControl public RawMaterialEntryOperationView() { InitializeComponent(); + PrintPreviewWebView.CreationProperties = WebView2UserDataFolder.CreateCreationProperties("RawMaterialEntryPreview"); Loaded += OnLoaded; DataContextChanged += OnDataContextChanged; Unloaded += (_, _) => DetachVm(); diff --git a/yy-admin-master/YY.Admin/YY.Admin.csproj b/yy-admin-master/YY.Admin/YY.Admin.csproj index d74804e..911083c 100644 --- a/yy-admin-master/YY.Admin/YY.Admin.csproj +++ b/yy-admin-master/YY.Admin/YY.Admin.csproj @@ -59,6 +59,8 @@ ..\YY.Admin.Core\libs\HandyControl.dll + + true @@ -106,10 +108,12 @@ PreserveNewest + + true - + Configuration\appsettings.json @@ -117,4 +121,13 @@ + + + <_AppSettingsSource>$(MSBuildProjectDirectory)\..\YY.Admin.Services\Configuration\appsettings.json + + + + + + diff --git a/yy-admin-master/_installer_output/YY.Admin_Setup_1.1.0_win-x64.exe b/yy-admin-master/_installer_output/YY.Admin_Setup_1.1.0_win-x64.exe new file mode 100644 index 0000000..dc9280c Binary files /dev/null and b/yy-admin-master/_installer_output/YY.Admin_Setup_1.1.0_win-x64.exe differ diff --git a/yy-admin-master/installer/YY.Admin.Setup.iss b/yy-admin-master/installer/YY.Admin.Setup.iss new file mode 100644 index 0000000..9fa4467 --- /dev/null +++ b/yy-admin-master/installer/YY.Admin.Setup.iss @@ -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; diff --git a/yy-admin-master/installer/build-installer.ps1 b/yy-admin-master/installer/build-installer.ps1 new file mode 100644 index 0000000..c1a9d72 --- /dev/null +++ b/yy-admin-master/installer/build-installer.ps1 @@ -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" diff --git a/yy-admin-master/installer/redist/.gitignore b/yy-admin-master/installer/redist/.gitignore new file mode 100644 index 0000000..c44c892 --- /dev/null +++ b/yy-admin-master/installer/redist/.gitignore @@ -0,0 +1,2 @@ +# WebView2 Evergreen 引导程序由 build-installer.ps1 自动下载,不必提交仓库 +MicrosoftEdgeWebview2Setup.exe