diff --git a/XP.Common/Configs/SerilogConfig.cs b/XP.Common/Configs/SerilogConfig.cs
new file mode 100644
index 0000000..8fa1fad
--- /dev/null
+++ b/XP.Common/Configs/SerilogConfig.cs
@@ -0,0 +1,43 @@
+using System;
+using System.IO;
+
+namespace XP.Common.Configs
+{
+ ///
+ /// Serilog日志配置实体(从App.config读取)
+ ///
+ public class SerilogConfig
+ {
+ ///
+ /// 日志输出根路径(默认:AppData/Files/Logs)
+ ///
+ public string LogPath { get; set; } = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
+ "Files", "Logs");
+
+ ///
+ /// 最低日志级别(Debug/Info/Warn/Error/Fatal)
+ ///
+ public string MinimumLevel { get; set; } = "Info";
+
+ ///
+ /// 是否输出到控制台(调试环境=true,生产环境=false)
+ ///
+ public bool EnableConsole { get; set; } = true;
+
+ ///
+ /// 日志文件分割规则(Day/Month/Hour/Size)
+ ///
+ public string RollingInterval { get; set; } = "Day";
+
+ ///
+ /// 单个日志文件最大大小(MB,仅Size分割时生效)
+ ///
+ public long FileSizeLimitMB { get; set; } = 100;
+
+ ///
+ /// 保留日志文件数量(默认30天)
+ ///
+ public int RetainedFileCountLimit { get; set; } = 30;
+ }
+}
\ No newline at end of file
diff --git a/XP.Common/Configs/SqliteConfig.cs b/XP.Common/Configs/SqliteConfig.cs
new file mode 100644
index 0000000..7f7c57d
--- /dev/null
+++ b/XP.Common/Configs/SqliteConfig.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace XP.Common.Configs
+{
+ ///
+ /// SQLite 配置实体
+ ///
+ public class SqliteConfig
+ {
+ ///
+ /// 数据库文件路径
+ ///
+ public string DbFilePath { get; set; } = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
+ "Files", "Data", "XP.db");
+
+ ///
+ /// 连接超时时间(秒,默认30)
+ ///
+ public int ConnectionTimeout { get; set; } = 30;
+
+ ///
+ /// 数据库不存在时是否自动创建(默认true)
+ ///
+ public bool CreateIfNotExists { get; set; } = true;
+
+ ///
+ /// 是否启用 WAL 模式(提升并发性能,默认true)
+ ///
+ public bool EnableWalMode { get; set; } = true;
+
+ ///
+ /// 是否开启日志记录(记录所有SQL操作,默认false)
+ ///
+ public bool EnableSqlLogging { get; set; } = false;
+
+ ///
+ /// 获取SQLite连接字符串
+ ///
+ public string GetConnectionString()
+ {
+ var builder = new Microsoft.Data.Sqlite.SqliteConnectionStringBuilder
+ {
+ DataSource = DbFilePath,
+ Cache = Microsoft.Data.Sqlite.SqliteCacheMode.Default,
+ DefaultTimeout = ConnectionTimeout
+ };
+ return builder.ToString();
+ }
+ }
+}
diff --git a/XP.Common/Converters/ImageConverter.cs b/XP.Common/Converters/ImageConverter.cs
new file mode 100644
index 0000000..fb2b29f
--- /dev/null
+++ b/XP.Common/Converters/ImageConverter.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using Emgu.CV;
+using Emgu.CV.CvEnum;
+
+namespace XP.Common.Converters
+{
+ ///
+ /// 图像数据转换工具类,基于 Emgu CV 实现
+ ///
+ public static class ImageConverter
+ {
+ ///
+ /// 将 16 位无符号灰度数据转换为 RGB BitmapSource
+ /// 使用 Emgu CV 的 Normalize 进行线性拉伸,CvtColor 转 BGR
+ ///
+ /// 16 位灰度原始数据
+ /// 图像宽度
+ /// 图像高度
+ /// BGR24 格式的 BitmapSource
+ public static BitmapSource ConvertGray16ToBitmapSource(ushort[] data, int width, int height)
+ {
+ if (data == null) throw new ArgumentNullException(nameof(data));
+ if (width <= 0 || height <= 0) throw new ArgumentException("图像尺寸无效");
+
+ // 从 ushort[] 按字节拷贝构建 16 位灰度 Mat
+ using var mat16 = new Mat(height, width, DepthType.Cv16U, 1);
+ int byteCount = data.Length * sizeof(ushort);
+ unsafe
+ {
+ fixed (ushort* pData = data)
+ {
+ Buffer.MemoryCopy(pData, mat16.DataPointer.ToPointer(), byteCount, byteCount);
+ }
+ }
+
+ // 归一化到 0~255 并转为 8 位
+ using var mat8 = new Mat();
+ CvInvoke.Normalize(mat16, mat8, 0, 255, NormType.MinMax, DepthType.Cv8U);
+
+ // 灰度转 BGR(立式 CT 显示需要 RGB 格式)
+ using var matBgr = new Mat();
+ CvInvoke.CvtColor(mat8, matBgr, ColorConversion.Gray2Bgr);
+
+ // Mat 转 BitmapSource
+ return MatToBitmapSource(matBgr);
+ }
+
+ ///
+ /// 将 Emgu CV Mat 转换为 WPF BitmapSource
+ ///
+ private static BitmapSource MatToBitmapSource(Mat mat)
+ {
+ int width = mat.Width;
+ int height = mat.Height;
+ int channels = mat.NumberOfChannels;
+ int stride = mat.Step;
+
+ byte[] pixels = new byte[height * stride];
+ Marshal.Copy(mat.DataPointer, pixels, 0, pixels.Length);
+
+ var format = channels == 3 ? PixelFormats.Bgr24 : PixelFormats.Gray8;
+ return BitmapSource.Create(width, height, 96, 96, format, null, pixels, stride);
+ }
+ }
+}
diff --git a/XP.Common/Database/Helpers/PaginationHelper.cs b/XP.Common/Database/Helpers/PaginationHelper.cs
new file mode 100644
index 0000000..db11e6c
--- /dev/null
+++ b/XP.Common/Database/Helpers/PaginationHelper.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using XP.Common.Database.Models;
+
+namespace XP.Common.Database.Helpers
+{
+ ///
+ /// 分页计算辅助工具
+ ///
+ public static class PaginationHelper
+ {
+ ///
+ /// 计算分页偏移量(SQLite OFFSET)
+ ///
+ public static int CalculateOffset(PaginationRequest pagination)
+ {
+ if (pagination.PageIndex < 1) pagination.PageIndex = 1;
+ if (pagination.PageSize < 1) pagination.PageSize = 20;
+ return (pagination.PageIndex - 1) * pagination.PageSize;
+ }
+
+ ///
+ /// 验证并修正分页参数
+ ///
+ public static PaginationRequest ValidateAndFix(PaginationRequest pagination)
+ {
+ var fixedPagination = new PaginationRequest
+ {
+ PageIndex = pagination.PageIndex < 1 ? 1 : pagination.PageIndex,
+ PageSize = pagination.PageSize < 1 ? 20 : (pagination.PageSize > 1000 ? 1000 : pagination.PageSize),
+ OrderBy = pagination.OrderBy ?? string.Empty
+ };
+ return fixedPagination;
+ }
+ }
+}
diff --git a/XP.Common/Database/Helpers/SqlBuilderHelper.cs b/XP.Common/Database/Helpers/SqlBuilderHelper.cs
new file mode 100644
index 0000000..d895722
--- /dev/null
+++ b/XP.Common/Database/Helpers/SqlBuilderHelper.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace XP.Common.Database.Helpers
+{
+ ///
+ /// 通用CRUD SQL构建器(适配SQLite)
+ ///
+ public static class SqlBuilderHelper
+ {
+ ///
+ /// 构建插入SQL
+ ///
+ /// 表名
+ /// 列名集合
+ /// 插入SQL(参数名:@列名)
+ public static string BuildInsertSql(string tableName, IEnumerable columns)
+ {
+ if (string.IsNullOrEmpty(tableName)) throw new ArgumentNullException(nameof(tableName));
+ var columnList = columns?.ToList() ?? throw new ArgumentNullException(nameof(columns));
+ if (columnList.Count == 0) throw new ArgumentException("列名集合不能为空");
+
+ var columnsStr = string.Join(", ", columnList);
+ var paramsStr = string.Join(", ", columnList.Select(c => $"@{c}"));
+ return $"INSERT INTO {tableName} ({columnsStr}) VALUES ({paramsStr})";
+ }
+
+ ///
+ /// 构建更新SQL
+ ///
+ /// 表名
+ /// 更新列名
+ /// 条件列名(如Id)
+ /// 更新SQL
+ public static string BuildUpdateSql(string tableName, IEnumerable updateColumns, IEnumerable whereColumns)
+ {
+ if (string.IsNullOrEmpty(tableName)) throw new ArgumentNullException(nameof(tableName));
+ var updateList = updateColumns?.ToList() ?? throw new ArgumentNullException(nameof(updateColumns));
+ var whereList = whereColumns?.ToList() ?? throw new ArgumentNullException(nameof(whereColumns));
+ if (updateList.Count == 0) throw new ArgumentException("更新列名集合不能为空");
+ if (whereList.Count == 0) throw new ArgumentException("条件列名集合不能为空");
+
+ var updateStr = string.Join(", ", updateList.Select(c => $"{c}=@{c}"));
+ var whereStr = string.Join(" AND ", whereList.Select(c => $"{c}=@{c}"));
+ return $"UPDATE {tableName} SET {updateStr} WHERE {whereStr}";
+ }
+
+ ///
+ /// 构建删除SQL
+ ///
+ public static string BuildDeleteSql(string tableName, IEnumerable whereColumns)
+ {
+ if (string.IsNullOrEmpty(tableName)) throw new ArgumentNullException(nameof(tableName));
+ var whereList = whereColumns?.ToList() ?? throw new ArgumentNullException(nameof(whereColumns));
+ if (whereList.Count == 0) throw new ArgumentException("条件列名集合不能为空");
+
+ var whereStr = string.Join(" AND ", whereList.Select(c => $"{c}=@{c}"));
+ return $"DELETE FROM {tableName} WHERE {whereStr}";
+ }
+
+ ///
+ /// 构建查询SQL
+ ///
+ public static string BuildSelectSql(string tableName, IEnumerable columns, IEnumerable? whereColumns = null, string orderBy = "")
+ {
+ if (string.IsNullOrEmpty(tableName)) throw new ArgumentNullException(nameof(tableName));
+ var columnList = columns?.ToList() ?? throw new ArgumentNullException(nameof(columns));
+ if (columnList.Count == 0) columnList.Add("*");
+
+ var columnsStr = string.Join(", ", columnList);
+ var sql = new StringBuilder($"SELECT {columnsStr} FROM {tableName}");
+
+ // 添加WHERE条件
+ if (whereColumns != null && whereColumns.Any())
+ {
+ var whereStr = string.Join(" AND ", whereColumns.Select(c => $"{c}=@{c}"));
+ sql.Append($" WHERE {whereStr}");
+ }
+
+ // 添加排序
+ if (!string.IsNullOrEmpty(orderBy))
+ {
+ sql.Append($" ORDER BY {orderBy}");
+ }
+
+ return sql.ToString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/XP.Common/Database/Helpers/SqliteParameterHelper.cs b/XP.Common/Database/Helpers/SqliteParameterHelper.cs
new file mode 100644
index 0000000..555f4b7
--- /dev/null
+++ b/XP.Common/Database/Helpers/SqliteParameterHelper.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace XP.Common.Database.Helpers
+{
+ ///
+ /// SQLite参数化查询辅助工具
+ ///
+ public static class SqliteParameterHelper
+ {
+ ///
+ /// 创建参数字典
+ ///
+ /// 参数名-值对(如 ("Id", 1), ("Name", "Test"))
+ /// 参数字典
+ public static Dictionary CreateParameters(params (string Key, object Value)[] keyValues)
+ {
+ var parameters = new Dictionary();
+ foreach (var (key, value) in keyValues)
+ {
+ parameters.Add(key, value);
+ }
+ return parameters;
+ }
+
+ ///
+ /// 合并参数字典
+ ///
+ public static Dictionary MergeParameters(params Dictionary[] paramLists)
+ {
+ var merged = new Dictionary();
+ foreach (var paramList in paramLists)
+ {
+ foreach (var (key, value) in paramList)
+ {
+ if (!merged.ContainsKey(key))
+ {
+ merged.Add(key, value);
+ }
+ }
+ }
+ return merged;
+ }
+ }
+}
diff --git a/XP.Common/Database/Implementations/DbExecuteResult.cs b/XP.Common/Database/Implementations/DbExecuteResult.cs
new file mode 100644
index 0000000..55f8492
--- /dev/null
+++ b/XP.Common/Database/Implementations/DbExecuteResult.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using XP.Common.Database.Interfaces;
+
+namespace XP.Common.Database.Implementations
+{
+ ///
+ /// 数据库操作执行结果实体
+ ///
+ public class DbExecuteResult : IDbExecuteResult
+ {
+ public bool IsSuccess { get; set; }
+ public int RowsAffected { get; set; }
+ public string Message { get; set; } = string.Empty;
+ public Exception? Exception { get; set; }
+
+ ///
+ /// 快速创建成功结果
+ ///
+ public static DbExecuteResult Success(string message = "执行成功", int rowsAffected = 0)
+ {
+ return new DbExecuteResult
+ {
+ IsSuccess = true,
+ Message = message,
+ RowsAffected = rowsAffected
+ };
+ }
+
+ ///
+ /// 快速创建失败结果
+ ///
+ public static DbExecuteResult Fail(string message, Exception? ex = null, int rowsAffected = 0)
+ {
+ return new DbExecuteResult
+ {
+ IsSuccess = false,
+ Message = message,
+ Exception = ex,
+ RowsAffected = rowsAffected
+ };
+ }
+ }
+}
diff --git a/XP.Common/Database/Implementations/SqliteContext.cs b/XP.Common/Database/Implementations/SqliteContext.cs
new file mode 100644
index 0000000..33ef939
--- /dev/null
+++ b/XP.Common/Database/Implementations/SqliteContext.cs
@@ -0,0 +1,527 @@
+using Microsoft.Data.Sqlite;
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using XP.Common.Configs;
+using XP.Common.Database.Interfaces;
+using XP.Common.Database.Models;
+using XP.Common.Helpers;
+using XP.Common.Logging.Interfaces;
+
+using CustomDbTransaction = XP.Common.Database.Interfaces.IDbTransaction;
+
+namespace XP.Common.Database.Implementations
+{
+ ///
+ /// SQLite 核心操作实现(适配IDbContext)
+ ///
+ public class SqliteContext : IDbContext
+ {
+ private readonly SqliteConfig _config;
+ private readonly ILoggerService _logger;
+ private SqliteConnection? _connection;
+ private bool _isDisposed = false;
+
+ ///
+ /// 构造函数(DI注入配置+日志)
+ ///
+ public SqliteContext(SqliteConfig config, ILoggerService logger)
+ {
+ _config = config ?? throw new ArgumentNullException(nameof(config));
+ _logger = logger.ForModule("Sqlite.Context");
+
+ // 自动创建数据库目录
+ if (_config.CreateIfNotExists)
+ {
+ var dbDir = Path.GetDirectoryName(_config.DbFilePath);
+ if (!string.IsNullOrEmpty(dbDir) && !Directory.Exists(dbDir))
+ {
+ Directory.CreateDirectory(dbDir);
+ _logger.Info("自动创建数据库目录:{DirPath}", dbDir);
+ }
+ }
+ }
+
+ #region 连接管理
+ public IDbExecuteResult OpenConnection()
+ {
+ try
+ {
+ if (_connection != null && _connection.State == ConnectionState.Open)
+ {
+ return DbExecuteResult.Success("连接已打开");
+ }
+
+ _connection = new SqliteConnection(_config.GetConnectionString());
+ _connection.Open();
+ _logger.Debug("SQLite连接已打开:{DbPath}", _config.DbFilePath);
+
+ // 启用WAL模式提升性能
+ if (_config.EnableWalMode)
+ {
+ using var cmd = new SqliteCommand("PRAGMA journal_mode=WAL;", _connection);
+ cmd.ExecuteNonQuery();
+ _logger.Debug("SQLite已启用WAL模式");
+ }
+
+ return DbExecuteResult.Success("连接打开成功");
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "SQLite连接打开失败:{DbPath}", _config.DbFilePath);
+ return DbExecuteResult.Fail("连接打开失败", ex);
+ }
+ }
+
+ public async Task OpenConnectionAsync()
+ {
+ try
+ {
+ if (_connection != null && _connection.State == ConnectionState.Open)
+ {
+ return DbExecuteResult.Success("连接已打开");
+ }
+
+ _connection = new SqliteConnection(_config.GetConnectionString());
+ await _connection.OpenAsync();
+ _logger.Debug("SQLite连接已异步打开:{DbPath}", _config.DbFilePath);
+
+ if (_config.EnableWalMode)
+ {
+ using var cmd = new SqliteCommand("PRAGMA journal_mode=WAL;", _connection);
+ await cmd.ExecuteNonQueryAsync();
+ }
+
+ return DbExecuteResult.Success("连接异步打开成功");
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "SQLite连接异步打开失败:{DbPath}", _config.DbFilePath);
+ return DbExecuteResult.Fail("连接异步打开失败", ex);
+ }
+ }
+
+ ///
+ /// 获取有效连接(自动打开)
+ ///
+ private SqliteConnection GetValidConnection()
+ {
+ if (_connection == null || _connection.State != ConnectionState.Open)
+ {
+ var result = OpenConnection();
+ if (!result.IsSuccess)
+ {
+ throw new InvalidOperationException($"获取SQLite连接失败:{result.Message}");
+ }
+ }
+ return _connection!;
+ }
+ #endregion
+
+ #region 事务管理
+ public (IDbExecuteResult Result, CustomDbTransaction? Transaction) BeginTransaction()
+ {
+ try
+ {
+ var conn = GetValidConnection();
+ // 系统的SqliteTransaction
+ var innerSystemTrans = conn.BeginTransaction();
+ // 封装为自定义的SqliteTransaction
+ var customTrans = new SqliteTransaction(innerSystemTrans, _logger);
+ _logger.Debug("SQLite事务已开始");
+ // 返回:自定义IDbTransaction(解决返回类型不匹配)
+ return (DbExecuteResult.Success("事务开始成功"), customTrans);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "SQLite事务开始失败");
+ return (DbExecuteResult.Fail("事务开始失败", ex), null);
+ }
+ }
+ #endregion
+
+ #region 通用SQL执行
+ public IDbExecuteResult ExecuteNonQuery(string sql, Dictionary? parameters = null)
+ {
+ try
+ {
+ LogSql(sql, parameters);
+ using var cmd = CreateCommand(sql, parameters);
+ var rowsAffected = cmd.ExecuteNonQuery();
+ _logger.Debug("SQL执行成功,影响行数:{Rows}", rowsAffected);
+ return DbExecuteResult.Success("执行成功", rowsAffected);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "SQL执行失败:{Sql}", sql);
+ return DbExecuteResult.Fail("执行失败", ex);
+ }
+ }
+
+ public async Task ExecuteNonQueryAsync(string sql, Dictionary? parameters = null)
+ {
+ try
+ {
+ LogSql(sql, parameters);
+ using var cmd = CreateCommand(sql, parameters);
+ var rowsAffected = await cmd.ExecuteNonQueryAsync();
+ _logger.Debug("SQL异步执行成功,影响行数:{Rows}", rowsAffected);
+ return DbExecuteResult.Success("异步执行成功", (int)rowsAffected);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "SQL异步执行失败:{Sql}", sql);
+ return DbExecuteResult.Fail("异步执行失败", ex);
+ }
+ }
+
+ public (IDbExecuteResult Result, T? Value) ExecuteScalar(string sql, Dictionary? parameters = null)
+ {
+ try
+ {
+ LogSql(sql, parameters);
+ using var cmd = CreateCommand(sql, parameters);
+ var resultObj = cmd.ExecuteScalar();
+ T? value = default;
+ if (resultObj != null && resultObj != DBNull.Value)
+ {
+ if (resultObj is T directValue)
+ {
+ value = directValue;
+ }
+ else
+ {
+ // 类型不匹配时安全转换,处理值类型/引用类型
+ value = (T)Convert.ChangeType(resultObj, Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T));
+ }
+ }
+ var logValue = value ?? (object)"null";
+ _logger.Debug("SQL标量查询成功,返回值:{Value}", logValue);
+ return (DbExecuteResult.Success("标量查询成功"), value);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "SQL标量查询失败:{Sql}", sql);
+ return (DbExecuteResult.Fail("标量查询失败", ex), default);
+ }
+ }
+
+ public async Task<(IDbExecuteResult Result, T? Value)> ExecuteScalarAsync(string sql, Dictionary? parameters = null)
+ {
+ try
+ {
+ LogSql(sql, parameters);
+ using var cmd = CreateCommand(sql, parameters);
+ var resultObj = await cmd.ExecuteScalarAsync();
+ T? value = default;
+ if (resultObj != null && resultObj != DBNull.Value)
+ {
+ if (resultObj is T directValue)
+ {
+ value = directValue;
+ }
+ else
+ {
+ // 类型不匹配时安全转换,处理值类型/引用类型
+ value = (T)Convert.ChangeType(resultObj, Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T));
+ }
+ }
+ var logValue = value ?? (object)"null";
+ _logger.Debug("SQL异步标量查询成功,返回值:{Value}", logValue);
+ return (DbExecuteResult.Success("异步标量查询成功"), value);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "SQL异步标量查询失败:{Sql}", sql);
+ return (DbExecuteResult.Fail("异步标量查询失败", ex), default);
+ }
+ }
+ #endregion
+
+ #region 查询结果映射
+ public (IDbExecuteResult Result, DataTable? Data) ExecuteDataTable(string sql, Dictionary? parameters = null)
+ {
+ try
+ {
+ LogSql(sql, parameters);
+ using var cmd = CreateCommand(sql, parameters);
+ using var reader = cmd.ExecuteReader();
+
+ var dt = new DataTable();
+ // 添加列
+ for (int i = 0; i < reader.FieldCount; i++)
+ {
+ dt.Columns.Add(reader.GetName(i), reader.GetFieldType(i));
+ }
+ // 添加行
+ while (reader.Read())
+ {
+ var row = dt.NewRow();
+ for (int i = 0; i < reader.FieldCount; i++)
+ {
+ row[i] = reader.IsDBNull(i) ? DBNull.Value : reader.GetValue(i);
+ }
+ dt.Rows.Add(row);
+ }
+
+ _logger.Debug("SQL查询成功,返回DataTable行数:{Rows}", dt.Rows.Count);
+ return (DbExecuteResult.Success("查询成功"), dt);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "SQL查询DataTable失败:{Sql}", sql);
+ return (DbExecuteResult.Fail("查询失败", ex), null);
+ }
+ }
+
+ public async Task<(IDbExecuteResult Result, DataTable? Data)> ExecuteDataTableAsync(string sql, Dictionary? parameters = null)
+ {
+ try
+ {
+ LogSql(sql, parameters);
+ using var cmd = CreateCommand(sql, parameters);
+ using var reader = await cmd.ExecuteReaderAsync();
+
+ var dt = new DataTable();
+ // 添加列
+ for (int i = 0; i < reader.FieldCount; i++)
+ {
+ dt.Columns.Add(reader.GetName(i), reader.GetFieldType(i));
+ }
+ // 添加行(异步读取)
+ while (await reader.ReadAsync())
+ {
+ var row = dt.NewRow();
+ for (int i = 0; i < reader.FieldCount; i++)
+ {
+ row[i] = await reader.IsDBNullAsync(i) ? DBNull.Value : reader.GetValue(i);
+ }
+ dt.Rows.Add(row);
+ }
+ _logger.Debug("SQL异步查询成功,返回DataTable行数:{Rows}", dt.Rows.Count);
+ return (DbExecuteResult.Success("异步查询成功"), dt);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "SQL异步查询DataTable失败:{Sql}", sql);
+ return (DbExecuteResult.Fail("异步查询失败", ex), null);
+ }
+ }
+
+ public (IDbExecuteResult Result, List Data) QueryList(string sql, Dictionary? parameters = null) where T : new()
+ {
+ try
+ {
+ var (result, dt) = ExecuteDataTable(sql, parameters);
+ if (!result.IsSuccess || dt == null)
+ {
+ return (result, new List());
+ }
+
+ var list = MapDataTableToEntityList(dt);
+ return (DbExecuteResult.Success("实体列表查询成功"), list);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "SQL查询实体列表失败:{Sql}", sql);
+ return (DbExecuteResult.Fail("实体列表查询失败", ex), new List());
+ }
+ }
+
+ public async Task<(IDbExecuteResult Result, List Data)> QueryListAsync(string sql, Dictionary? parameters = null) where T : new()
+ {
+ try
+ {
+ var (result, dt) = await ExecuteDataTableAsync(sql, parameters);
+ if (!result.IsSuccess || dt == null)
+ {
+ return (result, new List());
+ }
+
+ var list = MapDataTableToEntityList(dt);
+ return (DbExecuteResult.Success("异步实体列表查询成功"), list);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "SQL异步查询实体列表失败:{Sql}", sql);
+ return (DbExecuteResult.Fail("异步实体列表查询失败", ex), new List());
+ }
+ }
+ #endregion
+
+ #region 分页查询
+ public (IDbExecuteResult Result, PaginationResponse Data) QueryPaged(string sql, PaginationRequest pagination, Dictionary? parameters = null) where T : new()
+ {
+ try
+ {
+ // 1. 查询总条数
+ var countSql = $"SELECT COUNT(*) FROM ({sql}) AS TotalCountQuery";
+ var (countResult, totalCount) = ExecuteScalar(countSql, parameters);
+ if (!countResult.IsSuccess)
+ {
+ return (countResult, new PaginationResponse());
+ }
+
+ // 2. 构建分页SQL(SQLite分页:LIMIT/OFFSET)
+ var offset = (pagination.PageIndex - 1) * pagination.PageSize;
+ var pagedSql = $"{sql} {(!string.IsNullOrEmpty(pagination.OrderBy) ? $"ORDER BY {pagination.OrderBy}" : "")} LIMIT {pagination.PageSize} OFFSET {offset}";
+
+ // 3. 查询当前页数据
+ var (dataResult, list) = QueryList(pagedSql, parameters);
+ if (!dataResult.IsSuccess)
+ {
+ return (dataResult, new PaginationResponse());
+ }
+
+ // 4. 封装分页结果
+ var pagedResponse = new PaginationResponse
+ {
+ PageIndex = pagination.PageIndex,
+ PageSize = pagination.PageSize,
+ TotalCount = totalCount,
+ Data = list
+ };
+
+ _logger.Debug("SQL分页查询成功:页码{Page},条数{Size},总条数{Total}",
+ pagination.PageIndex, pagination.PageSize, totalCount);
+ return (DbExecuteResult.Success("分页查询成功"), pagedResponse);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "SQL分页查询失败:{Sql}", sql);
+ return (DbExecuteResult.Fail("分页查询失败", ex), new PaginationResponse());
+ }
+ }
+
+ public async Task<(IDbExecuteResult Result, PaginationResponse Data)> QueryPagedAsync(string sql, PaginationRequest pagination, Dictionary? parameters = null) where T : new()
+ {
+ try
+ {
+ // 1. 查询总条数
+ var countSql = $"SELECT COUNT(*) FROM ({sql}) AS TotalCountQuery";
+ var (countResult, totalCount) = await ExecuteScalarAsync(countSql, parameters);
+ if (!countResult.IsSuccess)
+ {
+ return (countResult, new PaginationResponse());
+ }
+
+ // 2. 构建分页SQL
+ var offset = (pagination.PageIndex - 1) * pagination.PageSize;
+ var pagedSql = $"{sql} {(!string.IsNullOrEmpty(pagination.OrderBy) ? $"ORDER BY {pagination.OrderBy}" : "")} LIMIT {pagination.PageSize} OFFSET {offset}";
+
+ // 3. 查询当前页数据
+ var (dataResult, list) = await QueryListAsync(pagedSql, parameters);
+ if (!dataResult.IsSuccess)
+ {
+ return (dataResult, new PaginationResponse());
+ }
+
+ // 4. 封装分页结果
+ var pagedResponse = new PaginationResponse
+ {
+ PageIndex = pagination.PageIndex,
+ PageSize = pagination.PageSize,
+ TotalCount = totalCount,
+ Data = list
+ };
+
+ _logger.Debug("SQL异步分页查询成功:页码{Page},条数{Size},总条数{Total}",
+ pagination.PageIndex, pagination.PageSize, totalCount);
+ return (DbExecuteResult.Success("异步分页查询成功"), pagedResponse);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "SQL异步分页查询失败:{Sql}", sql);
+ return (DbExecuteResult.Fail("异步分页查询失败", ex), new PaginationResponse());
+ }
+ }
+ #endregion
+
+ #region 私有辅助方法
+ ///
+ /// 创建SQLite命令(带参数)
+ ///
+ private SqliteCommand CreateCommand(string sql, Dictionary? parameters = null)
+ {
+ var cmd = new SqliteCommand(sql, GetValidConnection());
+ cmd.CommandTimeout = _config.ConnectionTimeout;
+
+ // 添加参数(防SQL注入)
+ if (parameters != null && parameters.Count > 0)
+ {
+ foreach (var (key, value) in parameters)
+ {
+ var paramValue = value ?? DBNull.Value;
+ cmd.Parameters.AddWithValue($"@{key}", paramValue);
+ }
+ }
+
+ return cmd;
+ }
+
+ ///
+ /// 记录SQL日志(调试模式)
+ ///
+ private void LogSql(string sql, Dictionary? parameters = null)
+ {
+ if (!_config.EnableSqlLogging) return;
+
+ var paramStr = parameters == null
+ ? "无参数"
+ : string.Join(", ", parameters.Select(kv => $"{kv.Key}={kv.Value}"));
+ _logger.Debug("执行SQL:{Sql} | 参数:{Params}", sql, paramStr);
+ }
+
+ ///
+ /// DataTable映射为实体列表
+ ///
+ private List MapDataTableToEntityList(DataTable dt) where T : new()
+ {
+ var list = new List();
+ var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
+
+ foreach (DataRow row in dt.Rows)
+ {
+ var entity = new T();
+ foreach (var prop in props)
+ {
+ if (dt.Columns.Contains(prop.Name) && row[prop.Name] != DBNull.Value)
+ {
+ var value = Convert.ChangeType(row[prop.Name], prop.PropertyType);
+ prop.SetValue(entity, value);
+ }
+ }
+ list.Add(entity);
+ }
+
+ return list;
+ }
+ #endregion
+
+ #region 资源释放
+ public void Dispose()
+ {
+ if (_isDisposed) return;
+
+ // 关闭连接
+ if (_connection != null)
+ {
+ if (_connection.State == ConnectionState.Open)
+ {
+ _connection.Close();
+ _logger.Debug("SQLite连接已关闭(Dispose)");
+ }
+ _connection.Dispose();
+ }
+
+ _isDisposed = true;
+ _logger.Debug("SQLiteContext资源已释放");
+ }
+ #endregion
+ }
+}
diff --git a/XP.Common/Database/Implementations/SqliteTransaction.cs b/XP.Common/Database/Implementations/SqliteTransaction.cs
new file mode 100644
index 0000000..c0266d1
--- /dev/null
+++ b/XP.Common/Database/Implementations/SqliteTransaction.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Data.Sqlite;
+using XP.Common.Database.Interfaces;
+using XP.Common.Logging.Interfaces;
+
+namespace XP.Common.Database.Implementations
+{
+ ///
+ /// SQLite 事务实现
+ ///
+ public class SqliteTransaction : IDbTransaction
+ {
+ private readonly Microsoft.Data.Sqlite.SqliteTransaction _innerTransaction;
+ private readonly ILoggerService _logger;
+ private bool _isDisposed = false;
+
+ public SqliteTransaction(Microsoft.Data.Sqlite.SqliteTransaction innerTrans, ILoggerService logger)
+ {
+ _innerTransaction = innerTrans ?? throw new ArgumentNullException(nameof(innerTrans));
+ _logger = logger.ForModule("Sqlite.Transaction");
+ }
+
+ public IDbExecuteResult Commit()
+ {
+ try
+ {
+ _innerTransaction.Commit();
+ _logger.Debug("SQLite事务提交成功");
+ return DbExecuteResult.Success("事务提交成功");
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "SQLite事务提交失败");
+ return DbExecuteResult.Fail("事务提交失败", ex);
+ }
+ }
+
+ public IDbExecuteResult Rollback()
+ {
+ try
+ {
+ _innerTransaction.Rollback();
+ _logger.Debug("SQLite事务回滚成功");
+ return DbExecuteResult.Success("事务回滚成功");
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "SQLite事务回滚失败");
+ return DbExecuteResult.Fail("事务回滚失败", ex);
+ }
+ }
+
+ public void Dispose()
+ {
+ if (_isDisposed) return;
+ _innerTransaction.Dispose();
+ _isDisposed = true;
+ _logger.Debug("SQLite事务资源已释放");
+ }
+ }
+}
\ No newline at end of file
diff --git a/XP.Common/Database/Interfaces/IDbContext.cs b/XP.Common/Database/Interfaces/IDbContext.cs
new file mode 100644
index 0000000..8cab642
--- /dev/null
+++ b/XP.Common/Database/Interfaces/IDbContext.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using XP.Common.Database.Models;
+
+namespace XP.Common.Database.Interfaces
+{
+ ///
+ /// 通用数据库操作接口(适配任意数据库,SQLite为具体实现)
+ ///
+ public interface IDbContext : IDisposable
+ {
+ ///
+ /// 打开数据库连接
+ ///
+ IDbExecuteResult OpenConnection();
+
+ ///
+ /// 异步打开数据库连接
+ ///
+ Task OpenConnectionAsync();
+
+ ///
+ /// 开始事务
+ ///
+ (IDbExecuteResult Result, IDbTransaction? Transaction) BeginTransaction();
+
+ ///
+ /// 执行增删改SQL(无返回值)
+ ///
+ /// SQL语句(参数化)
+ /// 参数集合(key=参数名,value=参数值)
+ IDbExecuteResult ExecuteNonQuery(string sql, Dictionary? parameters = null);
+
+ ///
+ /// 异步执行增删改SQL
+ ///
+ Task ExecuteNonQueryAsync(string sql, Dictionary? parameters = null);
+
+ ///
+ /// 执行查询并返回单个值(如Count/Sum)
+ ///
+ /// 返回值类型
+ /// SQL语句
+ /// 参数集合
+ (IDbExecuteResult Result, T? Value) ExecuteScalar(string sql, Dictionary? parameters = null);
+
+ ///
+ /// 异步执行查询并返回单个值
+ ///
+ Task<(IDbExecuteResult Result, T? Value)> ExecuteScalarAsync(string sql, Dictionary? parameters = null);
+
+ ///
+ /// 执行查询并返回DataTable
+ ///
+ (IDbExecuteResult Result, DataTable? Data) ExecuteDataTable(string sql, Dictionary? parameters = null);
+
+ ///
+ /// 异步执行查询并返回DataTable
+ ///
+ Task<(IDbExecuteResult Result, DataTable? Data)> ExecuteDataTableAsync(string sql, Dictionary? parameters = null);
+
+ ///
+ /// 执行查询并映射为实体列表
+ ///
+ /// 实体类型(需有无参构造函数)
+ (IDbExecuteResult Result, List Data) QueryList(string sql, Dictionary? parameters = null) where T : new();
+
+ ///
+ /// 异步执行查询并映射为实体列表
+ ///
+ Task<(IDbExecuteResult Result, List Data)> QueryListAsync(string sql, Dictionary? parameters = null) where T : new();
+
+ ///
+ /// 执行分页查询并返回分页结果
+ ///
+ /// 实体类型
+ (IDbExecuteResult Result, PaginationResponse Data) QueryPaged(string sql, PaginationRequest pagination, Dictionary? parameters = null) where T : new();
+
+ ///
+ /// 异步执行分页查询并返回分页结果
+ ///
+ Task<(IDbExecuteResult Result, PaginationResponse Data)> QueryPagedAsync(string sql, PaginationRequest pagination, Dictionary? parameters = null) where T : new();
+ }
+}
\ No newline at end of file
diff --git a/XP.Common/Database/Interfaces/IDbExecuteResult.cs b/XP.Common/Database/Interfaces/IDbExecuteResult.cs
new file mode 100644
index 0000000..9fdefc5
--- /dev/null
+++ b/XP.Common/Database/Interfaces/IDbExecuteResult.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace XP.Common.Database.Interfaces
+{
+ ///
+ /// 数据库操作通用执行结果
+ ///
+ public interface IDbExecuteResult
+ {
+ ///
+ /// 是否执行成功
+ ///
+ bool IsSuccess { get; set; }
+
+ ///
+ /// 影响行数(增删改)
+ ///
+ int RowsAffected { get; set; }
+
+ ///
+ /// 消息(成功/失败提示)
+ ///
+ string Message { get; set; }
+
+ ///
+ /// 异常信息(失败时非空)
+ ///
+ Exception? Exception { get; set; }
+ }
+}
diff --git a/XP.Common/Database/Interfaces/IDbTransaction.cs b/XP.Common/Database/Interfaces/IDbTransaction.cs
new file mode 100644
index 0000000..cf0c862
--- /dev/null
+++ b/XP.Common/Database/Interfaces/IDbTransaction.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace XP.Common.Database.Interfaces
+{
+ ///
+ /// 数据库事务接口
+ ///
+ public interface IDbTransaction : IDisposable
+ {
+ ///
+ /// 提交事务
+ ///
+ IDbExecuteResult Commit();
+
+ ///
+ /// 回滚事务
+ ///
+ IDbExecuteResult Rollback();
+ }
+}
diff --git a/XP.Common/Database/Models/PaginationRequest.cs b/XP.Common/Database/Models/PaginationRequest.cs
new file mode 100644
index 0000000..f5aa086
--- /dev/null
+++ b/XP.Common/Database/Models/PaginationRequest.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace XP.Common.Database.Models
+{
+ ///
+ /// 通用分页请求模型
+ ///
+ public class PaginationRequest
+ {
+ ///
+ /// 页码(从1开始)
+ ///
+ public int PageIndex { get; set; } = 1;
+
+ ///
+ /// 每页条数
+ ///
+ public int PageSize { get; set; } = 20;
+
+ ///
+ /// 排序字段(如:CreateTime DESC)
+ ///
+ public string OrderBy { get; set; } = string.Empty;
+ }
+}
\ No newline at end of file
diff --git a/XP.Common/Database/Models/PaginationResponse.cs b/XP.Common/Database/Models/PaginationResponse.cs
new file mode 100644
index 0000000..6da1dfa
--- /dev/null
+++ b/XP.Common/Database/Models/PaginationResponse.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace XP.Common.Database.Models
+{
+ ///
+ /// 通用分页响应模型
+ ///
+ /// 数据类型
+ public class PaginationResponse
+ {
+ ///
+ /// 页码
+ ///
+ public int PageIndex { get; set; }
+
+ ///
+ /// 每页条数
+ ///
+ public int PageSize { get; set; }
+
+ ///
+ /// 总条数
+ ///
+ public int TotalCount { get; set; }
+
+ ///
+ /// 总页数
+ ///
+ public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize);
+
+ ///
+ /// 当前页数据
+ ///
+ public List Data { get; set; } = new List();
+
+ ///
+ /// 是否有下一页
+ ///
+ public bool HasNextPage => PageIndex < TotalPages;
+ }
+}
diff --git a/XP.Common/Documents/Dump.README.md b/XP.Common/Documents/Dump.README.md
new file mode 100644
index 0000000..11e06e3
--- /dev/null
+++ b/XP.Common/Documents/Dump.README.md
@@ -0,0 +1,183 @@
+# Dump 文件管理服务使用指南 | Dump File Management Service Usage Guide
+
+## 概述 | Overview
+
+XplorePlane 提供 Dump 文件管理功能,用于在应用程序崩溃或需要诊断时生成进程转储文件。支持三种触发方式:崩溃自动触发、定时触发和手动触发,并提供文件大小限制、自动清理和可配置存储路径等管理能力。
+
+Dump 功能通过 Windows `MiniDumpWriteDump` API 实现,作为 `XP.Common` 的子模块集成到 `CommonModule` 中。
+
+## 基本用法 | Basic Usage
+
+### 通过依赖注入获取服务 | Get Service via DI
+
+```csharp
+public class DiagnosticsService
+{
+ private readonly IDumpService _dumpService;
+ private readonly ILoggerService _logger;
+
+ public DiagnosticsService(IDumpService dumpService, ILoggerService logger)
+ {
+ _dumpService = dumpService ?? throw new ArgumentNullException(nameof(dumpService));
+ _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ ///
+ /// 手动生成 Mini Dump | Manually generate Mini Dump
+ ///
+ public void CaptureMiniDump()
+ {
+ var filePath = _dumpService.CreateMiniDump();
+ if (filePath != null)
+ {
+ _logger.Info("Mini Dump 已生成:{FilePath} | Mini Dump generated: {FilePath}", filePath);
+ }
+ }
+
+ ///
+ /// 手动生成 Full Dump(包含完整内存)| Manually generate Full Dump (full memory)
+ ///
+ public void CaptureFullDump()
+ {
+ var filePath = _dumpService.CreateFullDump();
+ if (filePath != null)
+ {
+ _logger.Info("Full Dump 已生成:{FilePath} | Full Dump generated: {FilePath}", filePath);
+ }
+ }
+}
+```
+
+## 触发方式 | Trigger Modes
+
+### 1. 崩溃自动触发 | Crash Auto Trigger
+
+服务启动后自动订阅 `AppDomain.CurrentDomain.UnhandledException` 和 `TaskScheduler.UnobservedTaskException`,无需额外配置。崩溃时自动生成 Mini Dump。
+
+### 2. 定时触发 | Scheduled Trigger
+
+在 `App.config` 中启用定时触发后,服务按配置的时间间隔周期性生成 Mini Dump:
+
+```xml
+
+
+```
+
+### 3. 手动触发 | Manual Trigger
+
+通过 `IDumpService` 接口的方法手动触发:
+
+```csharp
+// 生成 Mini Dump(线程信息 + 数据段 + 句柄信息)
+// Generate Mini Dump (thread info + data segments + handle data)
+string? miniPath = _dumpService.CreateMiniDump();
+
+// 生成 Full Dump(完整内存,仅手动触发允许)
+// Generate Full Dump (full memory, manual trigger only)
+string? fullPath = _dumpService.CreateFullDump();
+```
+
+## Dump 类型说明 | Dump Type Description
+
+| 类型 | 包含内容 | 文件大小 | 触发限制 |
+|------|----------|----------|----------|
+| Mini Dump | 线程信息、数据段、句柄信息 | 较小(受大小限制约束) | 所有触发方式 |
+| Full Dump | 进程完整内存 | 较大(无大小限制) | 仅手动触发 |
+
+> 非手动触发(崩溃、定时)请求 Full Dump 时,系统会自动降级为 Mini Dump。
+
+## 文件命名规则 | File Naming Convention
+
+格式:`XplorePlane_{yyyyMMdd_HHmm}_{TriggerType}.dmp`
+
+示例:
+- `XplorePlane_20260317_1530_Crash.dmp` — 崩溃触发
+- `XplorePlane_20260317_1600_Scheduled.dmp` — 定时触发
+- `XplorePlane_20260317_1645_Manual.dmp` — 手动触发
+
+## 自动清理 | Auto Cleanup
+
+- 服务启动时立即执行一次清理
+- 运行期间每 24 小时执行一次清理
+- 超过保留天数(默认 7 天)的 `.dmp` 文件会被自动删除
+- 单个文件删除失败不影响其余文件的清理
+
+## 配置 | Configuration
+
+在 `App.config` 的 `` 中配置:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+| 配置项 | 默认值 | 说明 |
+|--------|--------|------|
+| `Dump:StoragePath` | `D:\XplorePlane\Dump` | Dump 文件存储路径 |
+| `Dump:EnableScheduledDump` | `false` | 是否启用定时触发 |
+| `Dump:ScheduledIntervalMinutes` | `60` | 定时触发间隔(分钟) |
+| `Dump:MiniDumpSizeLimitMB` | `100` | Mini Dump 文件大小上限(MB),超过则删除 |
+| `Dump:RetentionDays` | `7` | 文件保留天数,超过则自动清理 |
+
+## 错误处理 | Error Handling
+
+Dump 功能遵循"记录并继续"原则,自身错误不影响主应用程序运行:
+
+- Dump 文件写入失败 → 记录错误日志,返回 `null`,不抛出异常
+- Mini Dump 超过大小限制 → 删除文件,记录警告
+- 存储目录创建失败 → 回退到默认路径 `D:\XplorePlane\Dump`
+- 清理过程中文件删除失败 → 记录错误,继续清理其余文件
+
+## IDumpService 接口 | IDumpService Interface
+
+```csharp
+public interface IDumpService : IDisposable
+{
+ ///
+ /// 手动触发 Mini Dump 生成 | Manually trigger Mini Dump generation
+ ///
+ string? CreateMiniDump();
+
+ ///
+ /// 手动触发 Full Dump 生成 | Manually trigger Full Dump generation
+ ///
+ string? CreateFullDump();
+
+ ///
+ /// 启动服务 | Start service
+ ///
+ void Start();
+
+ ///
+ /// 停止服务 | Stop service
+ ///
+ void Stop();
+}
+```
+
+## 文件结构 | File Structure
+
+```
+XP.Common/Dump/
+├── Interfaces/
+│ └── IDumpService.cs # 服务接口 | Service interface
+├── Implementations/
+│ ├── DumpService.cs # 服务实现 | Service implementation
+│ └── DumpCleaner.cs # 自动清理组件 | Auto cleanup component
+├── Configs/
+│ ├── DumpConfig.cs # 配置实体 | Config entity
+│ └── DumpTriggerType.cs # 触发类型枚举 | Trigger type enum
+└── Native/
+ ├── NativeMethods.cs # P/Invoke 声明 | P/Invoke declarations
+ └── MiniDumpType.cs # Dump 类型标志枚举 | Dump type flags enum
+```
diff --git a/XP.Common/Documents/GeneralForm.README.md b/XP.Common/Documents/GeneralForm.README.md
new file mode 100644
index 0000000..f187231
--- /dev/null
+++ b/XP.Common/Documents/GeneralForm.README.md
@@ -0,0 +1,247 @@
+# 通用窗体使用指南 | General Form Usage Guide
+
+## 概述 | Overview
+
+`XP.Common.GeneralForm` 提供 XplorePlane 项目中可复用的通用 WPF 窗体组件。当前包含以下窗体:
+
+| 窗体 | 说明 |
+|---|---|
+| `ProgressWindow` | 模态进度条窗口,支持线程安全的进度更新和关闭操作 |
+| `InputDialog` | 通用输入对话框,支持单行文本输入、可选验证和多语言按钮 |
+
+## 目录结构 | Directory Structure
+
+```
+XP.Common/GeneralForm/
+├── ViewModels/
+│ ├── InputDialogViewModel.cs # 输入对话框 ViewModel
+│ └── ProgressWindowViewModel.cs # 进度窗口 ViewModel
+└── Views/
+ ├── InputDialog.xaml # 输入对话框 XAML 视图
+ ├── InputDialog.xaml.cs # 输入对话框 Code-Behind
+ ├── ProgressWindow.xaml # 进度窗口 XAML 视图
+ └── ProgressWindow.xaml.cs # 进度窗口 Code-Behind
+```
+
+---
+
+## ProgressWindow 进度条窗口
+
+### 功能特性 | Features
+
+- 模态进度条窗口,居中显示,不可调整大小
+- 线程安全:`UpdateProgress()` 和 `Close()` 可从任意线程调用,内部自动通过 `Dispatcher` 调度
+- 可配置是否允许用户手动关闭窗口(`isCancelable` 参数)
+- 不可取消时,通过 Win32 API 禁用窗口关闭按钮(灰色不可点击)
+- 进度值自动 Clamp 到 `[0, 100]` 范围,超出范围时记录 Warn 日志
+- 自动继承主窗口图标
+- 使用 Telerik `RadProgressBar` 控件(Crystal 主题)
+
+### 构造函数参数 | Constructor Parameters
+
+```csharp
+public ProgressWindow(
+ string title = "操作进行中", // 窗口标题
+ string message = "请稍候...", // 提示信息
+ bool isCancelable = true, // 是否允许用户关闭窗口
+ ILoggerService? logger = null // 日志服务(可选)
+)
+```
+
+| 参数 | 类型 | 默认值 | 说明 |
+|---|---|---|---|
+| `title` | `string` | `"操作进行中"` | 窗口标题栏文本 |
+| `message` | `string` | `"请稍候..."` | 进度条上方的提示信息 |
+| `isCancelable` | `bool` | `true` | `true`:用户可手动关闭;`false`:禁用关闭按钮 |
+| `logger` | `ILoggerService?` | `null` | 传入日志服务后自动记录窗口生命周期日志 |
+
+### 公开方法 | Public Methods
+
+#### UpdateProgress - 更新进度
+
+```csharp
+// 线程安全,可从任意线程调用
+void UpdateProgress(string message, double progress)
+```
+
+- `message`:更新提示信息文本
+- `progress`:进度值(0-100),超出范围自动修正
+
+#### Close - 关闭窗口
+
+```csharp
+// 线程安全,可从任意线程调用(隐藏基类 Window.Close())
+new void Close()
+```
+
+### 基本用法 | Basic Usage
+
+```csharp
+using XP.Common.GeneralForm.Views;
+using XP.Common.Logging.Interfaces;
+
+public class SomeService
+{
+ private readonly ILoggerService _logger;
+
+ public SomeService(ILoggerService logger)
+ {
+ _logger = logger?.ForModule()
+ ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public async Task ExecuteLongOperation()
+ {
+ // 创建进度窗口(不可取消)
+ var progressWindow = new ProgressWindow(
+ title: "数据处理中",
+ message: "正在初始化...",
+ isCancelable: false,
+ logger: _logger);
+
+ // 显示模态窗口(需在 UI 线程调用)
+ // 注意:ShowDialog() 会阻塞,通常配合 Task 使用
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ progressWindow.UpdateProgress("正在加载数据...", 20);
+ await Task.Delay(1000); // 模拟耗时操作
+
+ progressWindow.UpdateProgress("正在处理数据...", 60);
+ await Task.Delay(1000);
+
+ progressWindow.UpdateProgress("即将完成...", 90);
+ await Task.Delay(500);
+
+ progressWindow.UpdateProgress("完成", 100);
+ }
+ finally
+ {
+ // 关闭窗口(线程安全)
+ progressWindow.Close();
+ }
+ });
+
+ progressWindow.ShowDialog();
+ }
+}
+```
+
+### 允许用户取消的用法 | Cancelable Usage
+
+```csharp
+// 创建可取消的进度窗口
+var progressWindow = new ProgressWindow(
+ title: "文件导出",
+ message: "正在导出文件...",
+ isCancelable: true, // 用户可以点击关闭按钮取消
+ logger: _logger);
+
+progressWindow.ShowDialog();
+```
+
+### ViewModel 绑定属性 | ViewModel Binding Properties
+
+`ProgressWindowViewModel` 继承自 `BindableBase`,提供以下可绑定属性:
+
+| 属性 | 类型 | 说明 |
+|---|---|---|
+| `Title` | `string` | 窗口标题(只读) |
+| `Message` | `string` | 提示信息文本(可通知) |
+| `Progress` | `double` | 进度值 0-100(可通知) |
+| `ProgressText` | `string` | 百分比显示文本,如 `"75%"`(只读,自动计算) |
+| `IsCancelable` | `bool` | 是否允许用户关闭(只读) |
+
+### 注意事项 | Notes
+
+1. `ShowDialog()` 必须在 UI 线程调用,它会阻塞当前线程直到窗口关闭
+2. `UpdateProgress()` 和 `Close()` 内部已处理跨线程调度,可安全地从后台线程调用
+3. 当 `isCancelable = false` 时,窗口关闭按钮会被 Win32 API 禁用(灰色),用户无法通过 Alt+F4 或点击关闭
+4. 进度值超出 `[0, 100]` 范围时会自动修正并记录 Warn 级别日志
+5. `Close()` 使用 `new` 关键字隐藏基类方法(因 `Window.Close()` 非虚方法),确保通过 `ProgressWindow` 类型引用调用
+
+---
+
+## InputDialog 输入对话框
+
+### 功能特性 | Features
+
+- 模态输入对话框,居中于父窗口显示,不可调整大小
+- 自动继承主窗口图标
+- 按钮文本支持多语言(使用 `Button_OK` / `Button_Cancel` 资源键)
+- 可选的输入验证委托(`Func`),验证失败时在输入框下方显示红色错误提示
+- 输入内容变化时自动清除验证错误
+- 使用 Telerik `RadWatermarkTextBox` 和 `RadButton` 控件(Crystal 主题)
+- 提供静态 `Show()` 便捷方法,一行代码即可调用
+
+### 静态方法 | Static Method
+
+```csharp
+public static string? Show(
+ string prompt, // 提示文本
+ string title, // 窗口标题
+ string defaultValue = "", // 默认值
+ Func? validate = null, // 验证委托(可选)
+ Window? owner = null // 父窗口(可选)
+)
+```
+
+| 参数 | 类型 | 默认值 | 说明 |
+|---|---|---|---|
+| `prompt` | `string` | 必填 | 输入框上方的提示文本 |
+| `title` | `string` | 必填 | 窗口标题栏文本 |
+| `defaultValue` | `string` | `""` | 输入框的初始值 |
+| `validate` | `Func?` | `null` | 验证委托:返回 `null` 表示通过,返回错误信息则阻止确认 |
+| `owner` | `Window?` | `null` | 父窗口,设置后对话框居中于父窗口 |
+
+返回值:用户输入的字符串,取消时返回 `null`。
+
+### 基本用法 | Basic Usage
+
+```csharp
+using XP.Common.GeneralForm.Views;
+
+// 最简用法,无验证
+var name = InputDialog.Show("请输入名称:", "新建项目");
+if (name == null) return; // 用户取消
+
+// 带默认值
+var port = InputDialog.Show("请输入端口号:", "配置", "8080");
+```
+
+### 带验证的用法 | Usage with Validation
+
+```csharp
+// 验证委托:返回 null 表示通过,返回错误信息则显示在输入框下方
+var groupId = InputDialog.Show(
+ "请输入 Group ID:",
+ "新增 Group",
+ validate: input =>
+ {
+ if (string.IsNullOrWhiteSpace(input))
+ return "ID 不能为空";
+ if (existingIds.Contains(input))
+ return "ID 已存在,请使用不同的 ID";
+ return null; // 验证通过
+ });
+
+// 验证非负整数
+var dbNumber = InputDialog.Show(
+ "请输入 DB 块号(非负整数):",
+ "新增 Group",
+ "0",
+ validate: input =>
+ {
+ if (!int.TryParse(input, out int val) || val < 0)
+ return "必须为非负整数";
+ return null;
+ });
+```
+
+### 注意事项 | Notes
+
+1. `Show()` 必须在 UI 线程调用(内部使用 `ShowDialog()`)
+2. 验证委托在用户点击确定按钮时执行,验证失败不会关闭对话框
+3. 用户修改输入内容时会自动清除上一次的验证错误提示
+4. 按钮文本从 XP.Common 资源文件读取(`Button_OK` / `Button_Cancel`),自动跟随应用语言
diff --git a/XP.Common/Documents/Localization.Guidance.md b/XP.Common/Documents/Localization.Guidance.md
new file mode 100644
index 0000000..9705943
--- /dev/null
+++ b/XP.Common/Documents/Localization.Guidance.md
@@ -0,0 +1,1176 @@
+# 多语言支持使用指南 | Localization Support Guide
+
+## 概述 | Overview
+
+XplorePlane 多语言支持系统基于 .NET 原生 Resx 资源文件实现,与 Prism MVVM 架构无缝集成。系统支持简体中文(zh-CN)、繁体中文(zh-TW)和美式英语(en-US)三种语言。
+
+### 核心特性 | Key Features
+
+- ✅ 基于 .NET Resx 资源文件,编译时类型安全
+- ✅ 简洁的 XAML 标记扩展语法
+- ✅ 完整的 ViewModel 集成支持
+- ✅ 语言设置持久化到 App.config
+- ✅ 跨模块事件通知机制
+- ✅ 健壮的错误处理和回退机制
+- ✅ 模块化资源管理:各子项目独立维护 resx,通过 Fallback Chain 自动查找(v2.0)
+- ✅ 线程安全的资源源注册/注销(v2.0)
+
+---
+
+## 快速开始 | Quick Start
+
+### 1. 在 XAML 中使用本地化资源 | Using Localization in XAML
+
+#### 基础用法 | Basic Usage
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+#### 命名空间声明 | Namespace Declaration
+
+在 XAML 文件顶部添加命名空间引用:
+
+```xml
+xmlns:loc="clr-namespace:XplorePlane.Common.Localization.Extensions;assembly=XP.Common"
+```
+
+#### 语法说明 | Syntax Explanation
+
+- `{loc:Localization Key=ResourceKey}` - 完整语法
+- `{loc:Localization App_Title}` - 简化语法(Key 可省略)
+- 资源键不存在时,显示键名本身(便于调试)
+
+---
+
+### 2. 在 C# 代码中使用静态帮助类 | Using Static Helper in C# Code
+
+#### LocalizationHelper 静态帮助类 | LocalizationHelper Static Helper
+
+适用于不方便依赖注入的场景,如静态方法、工具类、简单的代码调用等。
+
+文件位置:`XP.Common/Localization/Helpers/LocalizationHelper.cs`
+
+```csharp
+using XP.Common.Localization;
+
+// 基本用法 | Basic usage
+var title = LocalizationHelper.Get("App_Title");
+
+// 带格式化参数 | With format arguments
+var errorMsg = LocalizationHelper.Get("Settings_Language_SwitchFailed", ex.Message);
+```
+
+#### 初始化(V1.1 新增)| Initialization (New in V1.1)
+
+V1.1 版本新增 `Initialize()` 方法,建议在 `CommonModule` 或 App 启动时调用,使 `LocalizationHelper` 通过 `ILocalizationService` 获取字符串,从而支持 Fallback Chain 多资源源查找。
+
+```csharp
+// 在 CommonModule 或 App 启动时调用 | Call at CommonModule or App startup
+LocalizationHelper.Initialize(localizationService);
+```
+
+初始化后,`Get()` 优先通过 `ILocalizationService` 获取(支持 Fallback Chain);未初始化时兼容回退到原始 `ResourceManager`。
+
+#### 与 ILocalizationService 的区别 | Difference from ILocalizationService
+
+| 特性 | LocalizationHelper | ILocalizationService |
+|------|-------------------|---------------------|
+| 调用方式 | 静态方法,直接调用 | 依赖注入 |
+| 适用场景 | 工具类、静态方法、简单调用 | ViewModel、Service 等 DI 管理的类 |
+| 语言来源 | `CultureInfo.CurrentUICulture` | `CultureInfo.CurrentUICulture` |
+| 资源来源 | 初始化后走 Fallback Chain;未初始化时仅 XP.Common 资源 | Fallback Chain(所有已注册资源源) |
+| 找不到键时 | 返回键本身 | 返回键本身 + 记录警告日志 |
+| Fallback Chain 支持 | ✅ 需先调用 `Initialize()` | ✅ 原生支持 |
+
+#### 使用建议 | Usage Recommendations
+
+- 在 ViewModel / Service 中优先使用 `ILocalizationService`(可测试、可 Mock)
+- 在静态方法、工具类、或不方便注入的地方使用 `LocalizationHelper`
+- 建议在应用启动时调用 `LocalizationHelper.Initialize()` 以启用 Fallback Chain 支持
+- 两者初始化后读取同一套资源,结果一致
+
+---
+
+### 3. 在 ViewModel 中使用本地化服务 | Using Localization Service in ViewModel
+
+#### 依赖注入 | Dependency Injection
+
+```csharp
+using XplorePlane.Common.Localization.Interfaces;
+using XplorePlane.Common.Localization.Enums;
+using Prism.Mvvm;
+
+namespace XplorePlane.App.ViewModels
+{
+ public class MyViewModel : BindableBase
+ {
+ private readonly ILocalizationService _localizationService;
+ private readonly ILoggerService _logger;
+
+ public MyViewModel(
+ ILocalizationService localizationService,
+ ILoggerService logger)
+ {
+ _localizationService = localizationService;
+ _logger = logger;
+ }
+
+ // 获取本地化字符串 | Get localized string
+ public string GetWelcomeMessage()
+ {
+ return _localizationService.GetString("Welcome_Message");
+ }
+
+ // 获取当前语言 | Get current language
+ public SupportedLanguage CurrentLanguage => _localizationService.CurrentLanguage;
+ }
+}
+```
+
+#### 动态文本绑定 | Dynamic Text Binding
+
+```csharp
+public class StatusViewModel : BindableBase
+{
+ private readonly ILocalizationService _localizationService;
+ private string _statusMessage;
+
+ public string StatusMessage
+ {
+ get => _statusMessage;
+ set => SetProperty(ref _statusMessage, value);
+ }
+
+ public StatusViewModel(ILocalizationService localizationService)
+ {
+ _localizationService = localizationService;
+
+ // 订阅语言切换事件 | Subscribe to language changed event
+ _localizationService.LanguageChanged += OnLanguageChanged;
+
+ // 初始化状态消息 | Initialize status message
+ UpdateStatusMessage();
+ }
+
+ private void OnLanguageChanged(object sender, LanguageChangedEventArgs e)
+ {
+ // 语言切换时更新文本 | Update text when language changes
+ UpdateStatusMessage();
+ }
+
+ private void UpdateStatusMessage()
+ {
+ StatusMessage = _localizationService.GetString("Status_Ready");
+ }
+}
+```
+
+#### 数据验证消息本地化 | Localized Validation Messages
+
+```csharp
+public class FormViewModel : BindableBase, IDataErrorInfo
+{
+ private readonly ILocalizationService _localizationService;
+ private string _username;
+
+ public string Username
+ {
+ get => _username;
+ set => SetProperty(ref _username, value);
+ }
+
+ public string this[string columnName]
+ {
+ get
+ {
+ if (columnName == nameof(Username))
+ {
+ if (string.IsNullOrWhiteSpace(Username))
+ {
+ return _localizationService.GetString("Validation_UsernameRequired");
+ }
+ if (Username.Length < 3)
+ {
+ return _localizationService.GetString("Validation_UsernameTooShort");
+ }
+ }
+ return null;
+ }
+ }
+
+ public string Error => null;
+}
+```
+
+---
+
+### 4. 实现语言切换器 | Implementing Language Switcher
+
+#### ViewModel 实现 | ViewModel Implementation
+
+```csharp
+using XplorePlane.Common.Localization.Interfaces;
+using XplorePlane.Common.Localization.Enums;
+using XplorePlane.Common.Localization.ViewModels;
+using Prism.Mvvm;
+using System.Collections.Generic;
+using System.Windows;
+
+namespace XplorePlane.App.ViewModels
+{
+ public class SettingsViewModel : BindableBase
+ {
+ private readonly ILocalizationService _localizationService;
+ private readonly ILoggerService _logger;
+ private SupportedLanguage _selectedLanguage;
+
+ public SupportedLanguage SelectedLanguage
+ {
+ get => _selectedLanguage;
+ set
+ {
+ if (SetProperty(ref _selectedLanguage, value))
+ {
+ ChangeLanguage(value);
+ }
+ }
+ }
+
+ public IEnumerable AvailableLanguages { get; }
+
+ public SettingsViewModel(
+ ILocalizationService localizationService,
+ ILoggerService logger)
+ {
+ _localizationService = localizationService;
+ _logger = logger;
+ _selectedLanguage = localizationService.CurrentLanguage;
+
+ // 初始化可用语言列表 | Initialize available languages
+ AvailableLanguages = new[]
+ {
+ new LanguageOption(SupportedLanguage.ZhCN, "简体中文", "🇨🇳"),
+ new LanguageOption(SupportedLanguage.ZhTW, "繁體中文", "🇹🇼"),
+ new LanguageOption(SupportedLanguage.EnUS, "English", "🇺🇸")
+ };
+ }
+
+ private void ChangeLanguage(SupportedLanguage language)
+ {
+ try
+ {
+ _localizationService.SetLanguage(language);
+
+ // 提示用户重启应用 | Prompt user to restart
+ var message = _localizationService.GetString("Message_RestartRequired");
+ var title = _localizationService.GetString("Title_Notice");
+
+ MessageBox.Show(
+ message,
+ title,
+ MessageBoxButton.OK,
+ MessageBoxImage.Information);
+
+ _logger.Information($"Language changed to {language}");
+ }
+ catch (Exception ex)
+ {
+ _logger.Error($"Failed to change language to {language}", ex);
+
+ var errorMessage = _localizationService.GetString("Error_LanguageChangeFailed");
+ MessageBox.Show(
+ errorMessage,
+ "Error",
+ MessageBoxButton.OK,
+ MessageBoxImage.Error);
+ }
+ }
+ }
+}
+```
+
+#### XAML 视图 | XAML View
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+---
+
+### 5. 订阅语言切换事件 | Subscribing to Language Changed Events
+
+#### 使用 Prism EventAggregator | Using Prism EventAggregator
+
+```csharp
+using Prism.Events;
+using XplorePlane.Common.Localization.Events;
+
+public class MyViewModel : BindableBase
+{
+ private readonly IEventAggregator _eventAggregator;
+ private readonly ILocalizationService _localizationService;
+
+ public MyViewModel(
+ IEventAggregator eventAggregator,
+ ILocalizationService localizationService)
+ {
+ _eventAggregator = eventAggregator;
+ _localizationService = localizationService;
+
+ // 订阅语言切换事件 | Subscribe to language changed event
+ _eventAggregator.GetEvent()
+ .Subscribe(OnLanguageChanged, ThreadOption.UIThread);
+ }
+
+ private void OnLanguageChanged(LanguageChangedEventArgs args)
+ {
+ // 处理语言切换 | Handle language change
+ Console.WriteLine($"Language changed from {args.OldLanguage} to {args.NewLanguage}");
+
+ // 刷新 UI 文本 | Refresh UI text
+ RefreshLocalizedContent();
+ }
+
+ private void RefreshLocalizedContent()
+ {
+ // 更新所有需要本地化的属性 | Update all localized properties
+ RaisePropertyChanged(nameof(Title));
+ RaisePropertyChanged(nameof(Description));
+ }
+}
+```
+
+#### 使用 ILocalizationService 事件 | Using ILocalizationService Event
+
+```csharp
+public class MyViewModel : BindableBase
+{
+ private readonly ILocalizationService _localizationService;
+
+ public MyViewModel(ILocalizationService localizationService)
+ {
+ _localizationService = localizationService;
+
+ // 订阅服务事件 | Subscribe to service event
+ _localizationService.LanguageChanged += OnLanguageChanged;
+ }
+
+ private void OnLanguageChanged(object sender, LanguageChangedEventArgs e)
+ {
+ // 处理语言切换 | Handle language change
+ RefreshLocalizedContent();
+ }
+}
+```
+
+---
+
+### 6. 多资源源 Fallback Chain | Multi-Source Fallback Chain
+
+V1.1 版本引入了多资源源 Fallback Chain 机制,允许各 Prism 模块注册自己的 Resx 资源文件到统一的查找链中。
+
+#### 架构说明 | Architecture
+
+```
+Fallback Chain(查找顺序从右到左 | Lookup order from right to left):
+[XP.Common (默认)] → [XP.Scan (模块注册)] → [XP.Hardware (模块注册)]
+ ↑ 最高优先级 | Highest priority
+```
+
+- `XP.Common` 为默认资源源,始终位于 Chain[0],不可注销
+- 后注册的模块优先级更高(从末尾向前遍历)
+- 单个资源源查找异常时自动跳过,继续遍历下一个
+- 全部未找到时返回 key 本身并记录警告日志
+- 线程安全:内部使用 `ReaderWriterLockSlim` 保护读写操作
+
+#### 注册模块资源源 | Register Module Resource Source
+
+在 Prism 模块的 `OnInitialized` 中注册:
+
+```csharp
+using System.Resources;
+using XP.Common.Localization.Interfaces;
+
+public class ScanModule : IModule
+{
+ private readonly ILocalizationService _localizationService;
+
+ public ScanModule(ILocalizationService localizationService)
+ {
+ _localizationService = localizationService;
+ }
+
+ public void OnInitialized(IContainerProvider containerProvider)
+ {
+ // 注册模块资源源到 Fallback Chain | Register module resource source to Fallback Chain
+ var resourceManager = new ResourceManager(
+ "XP.Scan.Resources.Resources",
+ typeof(ScanModule).Assembly);
+ _localizationService.RegisterResourceSource("XP.Scan", resourceManager);
+ }
+
+ public void RegisterTypes(IContainerRegistry containerRegistry) { }
+}
+```
+
+#### 注销模块资源源 | Unregister Module Resource Source
+
+```csharp
+// 注销指定资源源 | Unregister specified resource source
+_localizationService.UnregisterResourceSource("XP.Scan");
+```
+
+#### 注意事项 | Notes
+
+| 场景 | 行为 |
+|------|------|
+| 重复注册同名资源源 | 抛出 `InvalidOperationException` |
+| 注销 `"XP.Common"` | 抛出 `InvalidOperationException`(禁止注销默认资源源) |
+| 注销不存在的名称 | 静默忽略,记录警告日志 |
+| 单个 ResourceManager 抛异常 | 捕获异常并继续遍历下一个资源源 |
+
+#### 模块资源文件结构 | Module Resource File Structure
+
+各模块应在自己的项目中创建独立的资源文件:
+
+```
+XP.Scan/
+├── Resources/
+│ ├── Resources.resx # 默认(简体中文)
+│ ├── Resources.zh-CN.resx # 简体中文
+│ ├── Resources.zh-TW.resx # 繁体中文
+│ └── Resources.en-US.resx # 英文
+```
+
+资源键建议使用模块前缀命名(如 `Scan_Button_Start`),避免与其他模块冲突。当多个资源源存在同名键时,后注册的模块优先。
+
+---
+
+## 添加新语言 | Adding New Languages
+
+### 步骤 1: 更新枚举定义 | Step 1: Update Enum Definition
+
+编辑 `XplorePlane.Common/Localization/Enums/SupportedLanguage.cs`:
+
+```csharp
+public enum SupportedLanguage
+{
+ [Description("zh-CN")]
+ ZhCN,
+
+ [Description("zh-TW")]
+ ZhTW,
+
+ [Description("en-US")]
+ EnUS,
+
+ // 添加新语言 | Add new language
+ [Description("ja-JP")]
+ JaJP, // 日语 | Japanese
+
+ [Description("ko-KR")]
+ KoKR // 韩语 | Korean
+}
+```
+
+### 步骤 2: 创建资源文件 | Step 2: Create Resource File
+
+1. 在 `XplorePlane.Common/Resources/` 目录下创建新的资源文件
+2. 文件命名格式:`Resources..resx`
+3. 例如:`Resources.ja-JP.resx`、`Resources.ko-KR.resx`
+
+### 步骤 3: 添加翻译内容 | Step 3: Add Translations
+
+在新资源文件中添加所有资源键的翻译:
+
+```xml
+
+
+
+ XplorePlane X線検査システム
+
+
+ ファイル
+
+
+
+```
+
+### 步骤 4: 更新 GetCultureInfo 方法 | Step 4: Update GetCultureInfo Method
+
+编辑 `ResxLocalizationService.cs` 中的 `GetCultureInfo` 方法:
+
+```csharp
+private CultureInfo GetCultureInfo(SupportedLanguage language)
+{
+ return language switch
+ {
+ SupportedLanguage.ZhCN => new CultureInfo("zh-CN"),
+ SupportedLanguage.ZhTW => new CultureInfo("zh-TW"),
+ SupportedLanguage.EnUS => new CultureInfo("en-US"),
+ SupportedLanguage.JaJP => new CultureInfo("ja-JP"), // 新增 | New
+ SupportedLanguage.KoKR => new CultureInfo("ko-KR"), // 新增 | New
+ _ => new CultureInfo("zh-CN")
+ };
+}
+```
+
+### 步骤 5: 更新系统默认语言逻辑 | Step 5: Update System Default Language Logic
+
+编辑 `LocalizationConfig.cs` 中的 `GetSystemDefaultLanguage` 方法:
+
+```csharp
+public SupportedLanguage GetSystemDefaultLanguage()
+{
+ try
+ {
+ var systemCulture = CultureInfo.CurrentUICulture.Name;
+
+ return systemCulture switch
+ {
+ "zh-CN" => SupportedLanguage.ZhCN,
+ "zh-TW" or "zh-HK" => SupportedLanguage.ZhTW,
+ "en-US" or "en" => SupportedLanguage.EnUS,
+ "ja-JP" or "ja" => SupportedLanguage.JaJP, // 新增 | New
+ "ko-KR" or "ko" => SupportedLanguage.KoKR, // 新增 | New
+ _ when systemCulture.StartsWith("zh") => SupportedLanguage.ZhCN,
+ _ when systemCulture.StartsWith("en") => SupportedLanguage.EnUS,
+ _ when systemCulture.StartsWith("ja") => SupportedLanguage.JaJP, // 新增 | New
+ _ when systemCulture.StartsWith("ko") => SupportedLanguage.KoKR, // 新增 | New
+ _ => SupportedLanguage.ZhCN
+ };
+ }
+ catch (Exception ex)
+ {
+ _logger.Error("Failed to get system default language", ex);
+ return SupportedLanguage.ZhCN;
+ }
+}
+```
+
+### 步骤 6: 测试新语言 | Step 6: Test New Language
+
+1. 编译项目确保无错误
+2. 运行应用程序
+3. 在语言切换器中选择新语言
+4. 重启应用程序验证新语言显示正确
+
+---
+
+## 常见问题 | FAQ
+
+### Q1: 为什么语言切换后界面没有更新?
+
+**A:** 语言切换需要重启应用程序才能生效。这是设计决策,避免了复杂的运行时 UI 刷新机制。
+
+解决方案:
+
+- 确保在语言切换后提示用户重启应用
+- 语言设置已保存到 App.config,重启后自动加载
+
+### Q2: 资源键不存在时会发生什么?
+
+**A:** 系统会返回资源键本身作为回退值,并记录警告日志。
+
+```csharp
+// 如果 "NonExistent_Key" 不存在
+var text = _localizationService.GetString("NonExistent_Key");
+// 返回: "NonExistent_Key"
+// 日志: Warning - Resource key 'NonExistent_Key' not found
+```
+
+### Q3: 如何在设计时预览本地化文本?
+
+**A:** LocalizationExtension 在设计时会返回 `[ResourceKey]` 格式的文本。
+
+```xml
+
+
+
+```
+
+### Q4: 可以在运行时动态添加资源吗?
+
+**A:** 单个 Resx 资源文件在编译时嵌入程序集,运行时只读。支持在运行时动态注册/注销模块级资源源(`RegisterResourceSource` / `UnregisterResourceSource`),实现模块级别的资源扩展。
+
+如需完全动态的资源(如用户自定义翻译),考虑:
+
+- 使用数据库存储翻译
+- 实现自定义 ILocalizationService
+- 使用外部 JSON/XML 配置文件
+
+### Q5: 如何处理带参数的本地化字符串?
+
+**A:** 使用 `string.Format` 或字符串插值:
+
+```csharp
+// 资源文件中定义
+// Welcome_User = "欢迎,{0}!"
+
+var userName = "张三";
+var message = string.Format(
+ _localizationService.GetString("Welcome_User"),
+ userName);
+// 结果: "欢迎,张三!"
+```
+
+### Q6: 多个模块如何共享本地化资源?
+
+**A:** V1.1 版本引入了多资源源 Fallback Chain 机制,各模块可以注册自己的 Resx 资源文件。
+
+所有模块通过依赖注入获取 `ILocalizationService`,共享统一的查找入口。各模块在 Prism 模块初始化时注册自己的 `ResourceManager`:
+
+```csharp
+// 在模块的 OnInitialized 中注册 | Register in module's OnInitialized
+public void OnInitialized(IContainerProvider containerProvider)
+{
+ var localizationService = containerProvider.Resolve();
+ var resourceManager = new ResourceManager(
+ "XP.Scan.Resources.Resources",
+ typeof(ScanModule).Assembly);
+ localizationService.RegisterResourceSource("XP.Scan", resourceManager);
+}
+```
+
+查找时从最后注册的资源源开始向前遍历,第一个返回非 null 值的即为结果。`XP.Common` 作为默认资源源始终兜底。
+
+通过命名约定区分资源键(如 `Scan_Button_Start`、`RaySource_StartSuccess`)仍然是推荐的最佳实践。
+
+### Q7: 如何确保所有语言的资源键一致?
+
+**A:** 建议使用工具或脚本验证:
+
+```csharp
+// 验证资源完整性的示例代码
+public class ResourceValidator
+{
+ public void ValidateResources()
+ {
+ var defaultKeys = GetResourceKeys("Resources.resx");
+ var zhTwKeys = GetResourceKeys("Resources.zh-TW.resx");
+ var enUsKeys = GetResourceKeys("Resources.en-US.resx");
+
+ var missingInZhTw = defaultKeys.Except(zhTwKeys);
+ var missingInEnUs = defaultKeys.Except(enUsKeys);
+
+ if (missingInZhTw.Any())
+ {
+ Console.WriteLine($"Missing in zh-TW: {string.Join(", ", missingInZhTw)}");
+ }
+
+ if (missingInEnUs.Any())
+ {
+ Console.WriteLine($"Missing in en-US: {string.Join(", ", missingInEnUs)}");
+ }
+ }
+
+ private HashSet GetResourceKeys(string resourceFile)
+ {
+ // 实现资源键提取逻辑
+ // Implementation for extracting resource keys
+ return new HashSet();
+ }
+}
+```
+
+---
+
+## 故障排除 | Troubleshooting
+
+### 问题 1: LocalizationExtension 未找到
+
+**症状:**
+
+```
+The type 'loc:Localization' was not found.
+```
+
+**解决方案:**
+
+1. 检查命名空间声明是否正确
+2. 确认程序集名称为 `XP.Common`
+3. 清理并重新编译解决方案
+
+```xml
+
+xmlns:loc="clr-namespace:XplorePlane.Common.Localization.Extensions;assembly=XP.Common"
+```
+
+### 问题 2: 语言设置未保存
+
+**症状:** 重启应用后语言恢复为默认值
+
+**解决方案:**
+
+1. 检查 App.config 文件是否存在
+2. 确认应用程序有写入配置文件的权限
+3. 查看日志中的错误信息
+
+```csharp
+// 检查配置文件路径
+var configPath = ConfigurationManager.OpenExeConfiguration(
+ ConfigurationUserLevel.None).FilePath;
+Console.WriteLine($"Config file: {configPath}");
+```
+
+### 问题 3: 资源文件未嵌入程序集
+
+**症状:** 运行时找不到资源
+
+**解决方案:**
+
+1. 检查 .csproj 文件中的资源文件配置
+2. 确认 Build Action 设置为 `Embedded Resource`
+3. 确认 Custom Tool 设置为 `ResXFileCodeGenerator`(仅 Resources.resx)
+
+```xml
+
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
+
+
+```
+
+### 问题 4: ILocalizationService 未注册
+
+**症状:**
+
+```
+Unable to resolve type 'ILocalizationService'
+```
+
+**解决方案:**
+
+1. 确认 CommonModule 已在 App.xaml.cs 中注册
+2. 检查 CommonModule.RegisterTypes 方法中的服务注册
+
+```csharp
+// App.xaml.cs
+protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
+{
+ moduleCatalog.AddModule();
+ // 其他模块...
+}
+
+// CommonModule.cs
+public void RegisterTypes(IContainerRegistry containerRegistry)
+{
+ containerRegistry.RegisterSingleton();
+ containerRegistry.RegisterSingleton();
+}
+```
+
+### 问题 5: 设计时显示 [ResourceKey]
+
+**症状:** XAML 设计器中显示 `[App_Title]` 而不是实际文本
+
+**解决方案:** 这是正常行为。LocalizationExtension 在设计时返回资源键格式,运行时返回实际翻译。
+
+如需设计时预览,可以使用 `d:DataContext` 提供设计时数据:
+
+```xml
+
+
+
+```
+
+---
+
+## 性能优化建议 | Performance Optimization Tips
+
+### 1. 避免频繁调用 GetString
+
+**不推荐:**
+
+```csharp
+// 每次属性访问都调用 GetString
+public string Title => _localizationService.GetString("App_Title");
+```
+
+**推荐:**
+
+```csharp
+// 缓存翻译结果
+private string _title;
+public string Title
+{
+ get => _title ??= _localizationService.GetString("App_Title");
+}
+
+// 语言切换时刷新缓存
+private void OnLanguageChanged(object sender, LanguageChangedEventArgs e)
+{
+ _title = null;
+ RaisePropertyChanged(nameof(Title));
+}
+```
+
+### 2. 使用 XAML 标记扩展而非代码
+
+**不推荐:**
+
+```csharp
+// 在 ViewModel 中设置文本
+public string ButtonText => _localizationService.GetString("Button_OK");
+```
+
+**推荐:**
+
+```xml
+
+
+```
+
+### 3. 批量更新 UI
+
+**不推荐:**
+
+```csharp
+private void OnLanguageChanged(object sender, LanguageChangedEventArgs e)
+{
+ RaisePropertyChanged(nameof(Title));
+ RaisePropertyChanged(nameof(Description));
+ RaisePropertyChanged(nameof(Status));
+ // ... 更多属性
+}
+```
+
+**推荐:**
+
+```csharp
+private void OnLanguageChanged(object sender, LanguageChangedEventArgs e)
+{
+ // 使用空字符串通知所有属性变更
+ RaisePropertyChanged(string.Empty);
+}
+```
+
+---
+
+## 最佳实践 | Best Practices
+
+### 1. 资源键命名约定
+
+```
+模块_功能_类型
+Module_Feature_Type
+
+示例 | Examples:
+- RaySource_StartSuccess (射线源模块 - 启动成功)
+- Detector_ConnectionFailed (探测器模块 - 连接失败)
+- Common_Button_OK (通用 - 按钮 - 确定)
+- Validation_UsernameRequired (验证 - 用户名必填)
+```
+
+### 2. 保持资源文件同步
+
+- 添加新资源键时,同时更新所有语言的资源文件
+- 使用版本控制跟踪资源文件变更
+- 定期运行资源完整性验证
+
+### 3. 提供上下文注释
+
+```xml
+
+ 保存
+ 通用保存按钮文本 | Generic save button text
+
+```
+
+### 4. 避免硬编码文本
+
+**不推荐:**
+
+```csharp
+MessageBox.Show("操作成功", "提示");
+```
+
+**推荐:**
+
+```csharp
+var message = _localizationService.GetString("Message_OperationSuccess");
+var title = _localizationService.GetString("Title_Notice");
+MessageBox.Show(message, title);
+```
+
+### 5. 使用有意义的资源键
+
+**不推荐:**
+
+```
+Text1, Text2, Label3
+```
+
+**推荐:**
+
+```
+App_Title, Menu_File, Button_OK
+```
+
+---
+
+## 模块化本地化(Modular Localization)| v2.0 新增
+
+### 背景 | Background
+
+v1.0 中所有多语言资源集中在 `XP.Common/Resources/Resources.resx`。随着子项目增多(XP.Scan、XP.Detector 等),集中式方案导致:
+
+- resx 文件过大,维护困难
+- 多人协作容易 Git 冲突
+- 模块间资源键混在一起,职责不清
+
+v2.0 引入 **Fallback Chain** 机制,每个子项目可以维护自己的 resx 资源文件,查找时先查模块自己的 resx,找不到再 fallback 到 XP.Common 的公共 resx。
+
+### 架构概览 | Architecture Overview
+
+```
+查找顺序(后注册 = 高优先级):
+
+GetString("Scan_Button_Start")
+ → [1] XP.Scan/Resources/Resources.resx ← 先查模块资源(找到,返回)
+ → [0] XP.Common/Resources/Resources.resx ← 找不到才 fallback
+
+GetString("App_Title")
+ → [1] XP.Scan/Resources/Resources.resx ← 模块里没有这个 key
+ → [0] XP.Common/Resources/Resources.resx ← fallback 到公共资源(找到,返回)
+```
+
+### 资源文件分层规范 | Resource File Layering
+
+| 层级 | 位置 | 内容 | 示例 |
+|------|------|------|------|
+| 公共层 | `XP.Common/Resources/` | 通用文案(按钮、状态、对话框等) | `App_Title`, `Button_OK`, `Status_Ready` |
+| 模块层 | `XP.Scan/Resources/` | 模块专属文案 | `Scan_Button_Start`, `Scan_Text_ScanMode` |
+| 模块层 | `XP.Detector/Resources/` | 模块专属文案 | `Detector_ConnectButton` |
+
+### 新增接口方法 | New Interface Methods
+
+`ILocalizationService` 新增两个方法:
+
+```csharp
+///
+/// 注册模块资源源到 Fallback Chain 末尾(最高优先级)
+///
+void RegisterResourceSource(string name, ResourceManager resourceManager);
+
+///
+/// 从 Fallback Chain 中注销指定资源源
+///
+void UnregisterResourceSource(string name);
+```
+
+### 子项目接入步骤 | Module Integration Steps
+
+以 XP.Scan 为例,完整接入只需 3 步:
+
+#### 步骤 1:创建模块 resx 文件
+
+在子项目下创建 `Resources/` 目录,添加 4 个 resx 文件:
+
+```
+XP.Scan/
+ Resources/
+ Resources.resx ← 默认语言(zh-CN)
+ Resources.zh-CN.resx ← 简体中文
+ Resources.en-US.resx ← 英文
+ Resources.zh-TW.resx ← 繁体中文
+```
+
+#### 步骤 2:配置 csproj
+
+在 `XP.Scan.csproj` 中添加嵌入资源配置:
+
+```xml
+
+
+ PublicResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+ Resources.resx
+
+
+ Resources.resx
+
+
+ Resources.resx
+
+
+
+
+ True
+ True
+ Resources.resx
+
+
+```
+
+#### 步骤 3:在 App 启动时注册资源源
+
+在 `App.xaml.cs` 的 `CreateShell()` 中注册模块的 ResourceManager:
+
+```csharp
+protected override Window CreateShell()
+{
+ var localizationService = Container.Resolve();
+ LocalizationExtension.Initialize(localizationService);
+ LocalizationHelper.Initialize(localizationService);
+
+ // 注册模块资源源
+ var scanResourceManager = new ResourceManager(
+ "XP.Scan.Resources.Resources", typeof(App).Assembly);
+ localizationService.RegisterResourceSource("XP.Scan", scanResourceManager);
+
+ return Container.Resolve();
+}
+```
+
+注意:`ResourceManager` 的第一个参数是嵌入资源的全名,格式为 `{RootNamespace}.Resources.Resources`。如果 csproj 没有显式设置 ``,默认使用程序集名称。
+
+### 资源键命名约定 | Resource Key Naming Convention
+
+格式:`模块名_功能_类型`
+
+| 模块 | 前缀 | 示例 |
+|------|------|------|
+| XP.Common(公共) | `App_`, `Button_`, `Status_`, `Menu_`, `Dialog_` | `App_Title`, `Button_OK` |
+| XP.Scan | `Scan_` | `Scan_Button_Start`, `Scan_Text_ScanMode` |
+| XP.Detector | `Detector_` | `Detector_ConnectButton` |
+| XP.RaySource | `RaySource_` | `RaySource_TurnOnButton` |
+
+规则:
+- 公共文案(多个模块共用的按钮、状态等)放在 `XP.Common/Resources/` 中
+- 模块专属文案放在各自项目的 `Resources/` 中
+- 模块资源键必须以模块名为前缀,避免跨模块冲突
+- 如果模块 resx 中定义了与 XP.Common 同名的 key,模块的值会覆盖公共值
+
+### XAML 用法(无变化)| XAML Usage (No Change)
+
+XAML 中的 `{loc:Localization}` 标记扩展用法完全不变,它通过 `ILocalizationService.GetString()` 查找,自动走 Fallback Chain:
+
+```xml
+
+
+
+
+
+```
+
+### LocalizationHelper 用法(无变化)| LocalizationHelper Usage (No Change)
+
+`LocalizationHelper.Get()` 也自动走 Fallback Chain,用法不变:
+
+```csharp
+// 自动从 Fallback Chain 查找
+var text = LocalizationHelper.Get("Scan_Button_Start");
+```
+
+前提是在 App 启动时调用过 `LocalizationHelper.Initialize(localizationService)`。
+
+### 线程安全 | Thread Safety
+
+Fallback Chain 使用 `ReaderWriterLockSlim` 保护:
+- `GetString` 获取读锁(多线程可并发读)
+- `RegisterResourceSource` / `UnregisterResourceSource` 获取写锁(互斥)
+- 读写不互斥时性能优于 `lock`
+
+### 错误处理 | Error Handling
+
+| 场景 | 行为 |
+|------|------|
+| `RegisterResourceSource` 传入 null | 抛出 `ArgumentNullException` |
+| 重复注册同名资源源 | 抛出 `InvalidOperationException` + 警告日志 |
+| 注销不存在的资源源 | 静默忽略 + 警告日志 |
+| 注销默认资源源 `"XP.Common"` | 抛出 `InvalidOperationException`(禁止) |
+| 所有资源源都找不到 key | 返回 key 本身 + 警告日志 |
+| 单个 ResourceManager 抛异常 | 捕获并继续查找下一个 |
+
+### 涉及的代码文件 | Modified Files
+
+| 文件 | 变更类型 | 说明 |
+|------|----------|------|
+| `XP.Common/Localization/Interfaces/ILocalizationService.cs` | 修改 | 新增 `RegisterResourceSource` / `UnregisterResourceSource` |
+| `XP.Common/Localization/Implementations/ResourceSource.cs` | 新增 | 资源源内部模型(name + ResourceManager) |
+| `XP.Common/Localization/Implementations/ResxLocalizationService.cs` | 修改 | Fallback Chain + ReaderWriterLockSlim |
+| `XP.Common/Localization/Helpers/LocalizationHelper.cs` | 修改 | 委托给 ILocalizationService + Initialize 方法 |
+
+### 向后兼容性 | Backward Compatibility
+
+- 未注册任何模块资源源时,行为与 v1.0 完全一致(仅使用 XP.Common 的 resx)
+- 现有 XAML 中的 `{loc:Localization}` 无需修改
+- 现有代码中的 `ILocalizationService.GetString()` 无需修改
+- 现有代码中的 `LocalizationHelper.Get()` 无需修改
+
+---
+
+## 相关文档 | Related Documentation
+
+---
+
+## 技术支持 | Technical Support
+
+如有问题或建议,请联系开发团队或查看项目文档。
+
+For questions or suggestions, please contact the development team or refer to the project documentation.
+
+---
+
+**版本 | Version:** 2.0
+**最后更新 | Last Updated:** 2026-04-01
diff --git a/XP.Common/Documents/Localization.README.md b/XP.Common/Documents/Localization.README.md
new file mode 100644
index 0000000..a7a578a
--- /dev/null
+++ b/XP.Common/Documents/Localization.README.md
@@ -0,0 +1,265 @@
+# 多语言支持快速开始 | Localization Quick Start
+
+## 概述 | Overview
+
+XplorePlane 多语言支持系统基于 .NET 原生 Resx 资源文件实现,与 Prism MVVM 架构无缝集成。系统支持简体中文(zh-CN)、繁体中文(zh-TW)和美式英语(en-US)三种语言。
+
+### 核心特性 | Key Features
+
+- ✅ 基于 .NET Resx 资源文件,编译时类型安全
+- ✅ 简洁的 XAML 标记扩展语法
+- ✅ 完整的 ViewModel 集成支持
+- ✅ 语言设置持久化到 App.config
+- ✅ 跨模块事件通知机制
+- ✅ 健壮的错误处理和回退机制
+- ✅ 多资源源 Fallback Chain 机制,支持模块级资源注册
+
+---
+
+## 快速开始 | Quick Start
+
+### 1. 在 XAML 中使用本地化资源 | Using Localization in XAML
+
+#### 基础用法 | Basic Usage
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+#### 命名空间声明 | Namespace Declaration
+
+在 XAML 文件顶部添加命名空间引用:
+
+```xml
+xmlns:loc="clr-namespace:XplorePlane.Common.Localization.Extensions;assembly=XP.Common"
+```
+
+#### 语法说明 | Syntax Explanation
+
+- `{loc:Localization Key=ResourceKey}` - 完整语法
+- `{loc:Localization App_Title}` - 简化语法(Key 可省略)
+- 资源键不存在时,显示键名本身(便于调试)
+
+---
+
+### 2. 在 C# 代码中使用静态帮助类 | Using Static Helper in C# Code
+
+适用于不方便依赖注入的场景(静态方法、工具类等)。
+
+```csharp
+using XP.Common.Localization;
+
+// 基本用法 | Basic usage
+var title = LocalizationHelper.Get("App_Title");
+
+// 带格式化参数 | With format arguments
+var errorMsg = LocalizationHelper.Get("Settings_Language_SwitchFailed", ex.Message);
+```
+
+- 在 ViewModel / Service 中优先使用 `ILocalizationService`(可测试、可 Mock)
+- 在静态方法、工具类、或不方便注入的地方使用 `LocalizationHelper`
+- 两者读取同一套 Resx 资源文件,结果一致
+
+> **V1.4.1.1 变更:** `LocalizationHelper` 新增 `Initialize(ILocalizationService)` 方法。初始化后,`Get()` 会优先通过 `ILocalizationService` 获取字符串(支持 Fallback Chain);未初始化时仍兼容回退到原始 `ResourceManager`。建议在 `CommonModule` 或 App 启动时调用初始化。
+
+```csharp
+// 在 CommonModule 或 App 启动时调用 | Call at CommonModule or App startup
+LocalizationHelper.Initialize(localizationService);
+```
+
+---
+
+### 3. 在 ViewModel 中使用本地化服务 | Using Localization Service in ViewModel
+
+#### 依赖注入 | Dependency Injection
+
+```csharp
+using XplorePlane.Common.Localization.Interfaces;
+using XplorePlane.Common.Localization.Enums;
+using Prism.Mvvm;
+
+namespace XplorePlane.App.ViewModels
+{
+ public class MyViewModel : BindableBase
+ {
+ private readonly ILocalizationService _localizationService;
+ private readonly ILoggerService _logger;
+
+ public MyViewModel(
+ ILocalizationService localizationService,
+ ILoggerService logger)
+ {
+ _localizationService = localizationService;
+ _logger = logger;
+ }
+
+ // 获取本地化字符串 | Get localized string
+ public string GetWelcomeMessage()
+ {
+ return _localizationService.GetString("Welcome_Message");
+ }
+
+ // 获取当前语言 | Get current language
+ public SupportedLanguage CurrentLanguage => _localizationService.CurrentLanguage;
+ }
+}
+```
+
+#### 动态文本绑定 | Dynamic Text Binding
+
+```csharp
+public class StatusViewModel : BindableBase
+{
+ private readonly ILocalizationService _localizationService;
+ private string _statusMessage;
+
+ public string StatusMessage
+ {
+ get => _statusMessage;
+ set => SetProperty(ref _statusMessage, value);
+ }
+
+ public StatusViewModel(ILocalizationService localizationService)
+ {
+ _localizationService = localizationService;
+
+ // 订阅语言切换事件 | Subscribe to language changed event
+ _localizationService.LanguageChanged += OnLanguageChanged;
+
+ // 初始化状态消息 | Initialize status message
+ UpdateStatusMessage();
+ }
+
+ private void OnLanguageChanged(object sender, LanguageChangedEventArgs e)
+ {
+ // 语言切换时更新文本 | Update text when language changes
+ UpdateStatusMessage();
+ }
+
+ private void UpdateStatusMessage()
+ {
+ StatusMessage = _localizationService.GetString("Status_Ready");
+ }
+}
+```
+
+#### 数据验证消息本地化 | Localized Validation Messages
+
+```csharp
+public class FormViewModel : BindableBase, IDataErrorInfo
+{
+ private readonly ILocalizationService _localizationService;
+ private string _username;
+
+ public string Username
+ {
+ get => _username;
+ set => SetProperty(ref _username, value);
+ }
+
+ public string this[string columnName]
+ {
+ get
+ {
+ if (columnName == nameof(Username))
+ {
+ if (string.IsNullOrWhiteSpace(Username))
+ {
+ return _localizationService.GetString("Validation_UsernameRequired");
+ }
+ if (Username.Length < 3)
+ {
+ return _localizationService.GetString("Validation_UsernameTooShort");
+ }
+ }
+ return null;
+ }
+ }
+
+ public string Error => null;
+}
+```
+
+---
+
+### 4. 多资源源 Fallback Chain | Multi-Source Fallback Chain
+
+V1.1 版本引入了多资源源 Fallback Chain 机制,允许各模块注册自己的 Resx 资源文件。查找资源键时,从最后注册的资源源开始向前遍历,第一个返回非 null 值的即为结果。
+
+#### 架构说明 | Architecture
+
+```
+Fallback Chain(查找顺序从右到左):
+[XP.Common (默认)] → [XP.Scan (模块注册)] → [XP.Hardware (模块注册)]
+ ↑ 最高优先级
+```
+
+- `XP.Common` 为默认资源源,始终位于 Chain[0],不可注销
+- 后注册的模块优先级更高
+- 单个资源源查找异常时自动跳过,继续遍历下一个
+- 全部未找到时返回 key 本身并记录警告日志
+
+#### 注册模块资源源 | Register Module Resource Source
+
+在 Prism 模块的 `OnInitialized` 中注册:
+
+```csharp
+using System.Resources;
+using XP.Common.Localization.Interfaces;
+
+public class ScanModule : IModule
+{
+ private readonly ILocalizationService _localizationService;
+
+ public ScanModule(ILocalizationService localizationService)
+ {
+ _localizationService = localizationService;
+ }
+
+ public void OnInitialized(IContainerProvider containerProvider)
+ {
+ // 注册模块资源源到 Fallback Chain
+ var resourceManager = new ResourceManager(
+ "XP.Scan.Resources.Resources",
+ typeof(ScanModule).Assembly);
+ _localizationService.RegisterResourceSource("XP.Scan", resourceManager);
+ }
+
+ public void RegisterTypes(IContainerRegistry containerRegistry) { }
+}
+```
+
+#### 注销模块资源源 | Unregister Module Resource Source
+
+```csharp
+// 注销指定资源源(不可注销默认的 "XP.Common")
+_localizationService.UnregisterResourceSource("XP.Scan");
+```
+
+#### 注意事项 | Notes
+
+- 资源源名称不可重复,重复注册会抛出 `InvalidOperationException`
+- 注销 `"XP.Common"` 会抛出 `InvalidOperationException`
+- 注销不存在的名称会静默忽略并记录警告日志
+- 线程安全:内部使用 `ReaderWriterLockSlim` 保护读写操作
+
+---
+
+**版本 | Version:** 1.1
+**最后更新 | Last Updated:** 2026-04-01
diff --git a/XP.Common/Documents/Logging.EXAMPLES.md b/XP.Common/Documents/Logging.EXAMPLES.md
new file mode 100644
index 0000000..f363725
--- /dev/null
+++ b/XP.Common/Documents/Logging.EXAMPLES.md
@@ -0,0 +1,233 @@
+# 日志服务使用示例 | Logger Service Usage Examples
+
+## 示例 1:服务类中使用 | Example 1: Usage in Service Class
+
+### 旧方式(手动传递字符串)| Old Way (Manual String)
+
+```csharp
+public class PlcService
+{
+ private readonly ILoggerService _logger;
+
+ public PlcService(ILoggerService logger)
+ {
+ // 需要手动输入类名,容易出错
+ // Need to manually type class name, error-prone
+ _logger = logger?.ForModule("PlcService") ?? throw new ArgumentNullException(nameof(logger));
+ }
+}
+
+// 日志输出 | Log output:
+// [PlcService] 正在初始化 PLC 连接...
+```
+
+### 新方式(自动类型推断)| New Way (Auto Type Inference)
+
+```csharp
+public class PlcService
+{
+ private readonly ILoggerService _logger;
+
+ public PlcService(ILoggerService logger)
+ {
+ // 自动获取完整类型名,重构安全
+ // Automatically get full type name, refactoring-safe
+ _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger));
+ }
+}
+
+// 日志输出 | Log output:
+// [XP.Hardware.Plc.Services.PlcService] 正在初始化 PLC 连接...
+```
+
+## 示例 2:ViewModel 中使用 | Example 2: Usage in ViewModel
+
+```csharp
+using Prism.Mvvm;
+using XP.Common.Logging.Interfaces;
+
+namespace XP.Hardware.RaySource.ViewModels
+{
+ public class RaySourceOperateViewModel : BindableBase
+ {
+ private readonly ILoggerService _logger;
+
+ public RaySourceOperateViewModel(ILoggerService logger)
+ {
+ // 自动使用:XP.Hardware.RaySource.ViewModels.RaySourceOperateViewModel
+ // Automatically uses: XP.Hardware.RaySource.ViewModels.RaySourceOperateViewModel
+ _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger));
+
+ _logger.Info("射线源操作视图模型已初始化 | Ray source operate view model initialized");
+ }
+
+ public void StartXRay()
+ {
+ _logger.Info("用户请求启动射线 | User requested to start X-ray");
+ // ... 业务逻辑 | business logic
+ }
+ }
+}
+```
+
+## 示例 3:工厂类中使用 | Example 3: Usage in Factory Class
+
+```csharp
+using XP.Common.Logging.Interfaces;
+using XP.Hardware.RaySource.Abstractions;
+
+namespace XP.Hardware.RaySource.Factories
+{
+ public class RaySourceFactory : IRaySourceFactory
+ {
+ private readonly ILoggerService _logger;
+
+ public RaySourceFactory(ILoggerService logger)
+ {
+ // 自动使用:XP.Hardware.RaySource.Factories.RaySourceFactory
+ // Automatically uses: XP.Hardware.RaySource.Factories.RaySourceFactory
+ _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public IXRaySource CreateRaySource(string deviceType)
+ {
+ _logger.Info("创建射线源实例:类型={DeviceType} | Creating ray source instance: type={DeviceType}", deviceType);
+
+ switch (deviceType)
+ {
+ case "Comet225":
+ return new Comet225RaySource(_logger);
+ default:
+ _logger.Error(null, "不支持的设备类型:{DeviceType} | Unsupported device type: {DeviceType}", deviceType);
+ throw new NotSupportedException($"不支持的设备类型:{deviceType}");
+ }
+ }
+ }
+}
+```
+
+## 示例 4:静态方法中使用 | Example 4: Usage in Static Methods
+
+```csharp
+public class ConfigLoader
+{
+ public static PlcConfig LoadConfig(ILoggerService logger)
+ {
+ // 静态方法中也可以使用泛型
+ // Can also use generics in static methods
+ var log = logger.ForModule();
+
+ log.Info("正在加载 PLC 配置 | Loading PLC configuration");
+
+ try
+ {
+ // ... 加载逻辑 | loading logic
+ log.Info("PLC 配置加载成功 | PLC configuration loaded successfully");
+ return config;
+ }
+ catch (Exception ex)
+ {
+ log.Error(ex, "PLC 配置加载失败 | PLC configuration loading failed");
+ throw;
+ }
+ }
+}
+```
+
+## 示例 5:嵌套类中使用 | Example 5: Usage in Nested Classes
+
+```csharp
+public class RaySourceService
+{
+ private readonly ILoggerService _logger;
+
+ public RaySourceService(ILoggerService logger)
+ {
+ _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ // 嵌套类 | Nested class
+ public class ConnectionManager
+ {
+ private readonly ILoggerService _logger;
+
+ public ConnectionManager(ILoggerService logger)
+ {
+ // 自动使用:XP.Hardware.RaySource.Services.RaySourceService+ConnectionManager
+ // Automatically uses: XP.Hardware.RaySource.Services.RaySourceService+ConnectionManager
+ _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger));
+ }
+ }
+}
+```
+
+## 示例 6:混合使用 | Example 6: Mixed Usage
+
+有时你可能想要自定义模块名以保持简洁:
+
+```csharp
+public class VeryLongNamespaceAndClassName
+{
+ private readonly ILoggerService _logger;
+
+ public VeryLongNamespaceAndClassName(ILoggerService logger)
+ {
+ // 选项 1:使用完整类型名(详细但冗长)
+ // Option 1: Use full type name (detailed but verbose)
+ // _logger = logger?.ForModule();
+ // 输出 | Output: [XP.Some.Very.Long.Namespace.VeryLongNamespaceAndClassName]
+
+ // 选项 2:使用简短自定义名(简洁但需手动维护)
+ // Option 2: Use short custom name (concise but needs manual maintenance)
+ _logger = logger?.ForModule("VeryLong") ?? throw new ArgumentNullException(nameof(logger));
+ // 输出 | Output: [VeryLong]
+ }
+}
+```
+
+## 优势对比 | Advantages Comparison
+
+### ForModule() 的优势 | Advantages of ForModule()
+
+✅ **重构安全**:重命名类时自动更新
+✅ **Refactoring-safe**: Automatically updates when renaming class
+
+✅ **无拼写错误**:编译器检查类型
+✅ **No typos**: Compiler checks type
+
+✅ **完整信息**:包含命名空间,便于定位
+✅ **Complete info**: Includes namespace, easy to locate
+
+✅ **智能提示**:IDE 自动补全
+✅ **IntelliSense**: IDE auto-completion
+
+### ForModule(string) 的优势 | Advantages of ForModule(string)
+
+✅ **简洁输出**:日志文件更易读
+✅ **Concise output**: Log files more readable
+
+✅ **自定义名称**:可以使用业务术语
+✅ **Custom names**: Can use business terms
+
+✅ **灵活性**:可以为不同场景使用不同名称
+✅ **Flexibility**: Can use different names for different scenarios
+
+## 推荐使用场景 | Recommended Usage Scenarios
+
+| 场景 | 推荐方式 | 原因 |
+|------|---------|------|
+| 服务类 | `ForModule()` | 需要完整追踪 |
+| ViewModel | `ForModule()` | 需要完整追踪 |
+| 工厂类 | `ForModule()` | 需要完整追踪 |
+| 简单工具类 | `ForModule("ToolName")` | 保持简洁 |
+| 临时调试 | `ForModule("Debug")` | 快速定位 |
+| 第三方集成 | `ForModule("ThirdParty.XXX")` | 明确标识 |
+
+| Scenario | Recommended | Reason |
+|----------|------------|--------|
+| Service classes | `ForModule()` | Need full tracing |
+| ViewModels | `ForModule()` | Need full tracing |
+| Factory classes | `ForModule()` | Need full tracing |
+| Simple utility classes | `ForModule("ToolName")` | Keep concise |
+| Temporary debugging | `ForModule("Debug")` | Quick location |
+| Third-party integration | `ForModule("ThirdParty.XXX")` | Clear identification |
diff --git a/XP.Common/Documents/Logging.README.md b/XP.Common/Documents/Logging.README.md
new file mode 100644
index 0000000..0faa70f
--- /dev/null
+++ b/XP.Common/Documents/Logging.README.md
@@ -0,0 +1,177 @@
+# 日志服务使用指南 | Logger Service Usage Guide
+
+## 概述 | Overview
+
+XplorePlane 使用 Serilog 作为底层日志框架,通过 `ILoggerService` 接口提供统一的日志服务。
+
+## 基本用法 | Basic Usage
+
+### 方式 1:自动类型推断(推荐)| Method 1: Auto Type Inference (Recommended)
+
+使用泛型方法 `ForModule()` 自动获取类型的完整名称(命名空间 + 类名):
+
+```csharp
+public class PlcService
+{
+ private readonly ILoggerService _logger;
+
+ public PlcService(ILoggerService logger)
+ {
+ // 自动使用 "XP.Hardware.Plc.Services.PlcService" 作为模块名
+ // Automatically uses "XP.Hardware.Plc.Services.PlcService" as module name
+ _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public void DoSomething()
+ {
+ _logger.Info("执行操作 | Performing operation");
+ }
+}
+```
+
+### 方式 2:手动指定模块名 | Method 2: Manual Module Name
+
+如果需要自定义模块名,可以使用字符串参数:
+
+```csharp
+public class PlcService
+{
+ private readonly ILoggerService _logger;
+
+ public PlcService(ILoggerService logger)
+ {
+ // 手动指定简短的模块名
+ // Manually specify a short module name
+ _logger = logger?.ForModule("PlcService") ?? throw new ArgumentNullException(nameof(logger));
+ }
+}
+```
+
+### 方式 3:使用 typeof 获取类型名 | Method 3: Using typeof for Type Name
+
+在静态方法或无法使用泛型的场景:
+
+```csharp
+public class PlcService
+{
+ private readonly ILoggerService _logger;
+
+ public PlcService(ILoggerService logger)
+ {
+ // 使用 typeof 获取类型
+ // Use typeof to get type
+ _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public static void StaticMethod(ILoggerService logger)
+ {
+ // 静态方法中也可以使用泛型
+ // Can also use generics in static methods
+ var log = logger.ForModule();
+ log.Info("静态方法日志 | Static method log");
+ }
+}
+```
+
+## 日志级别 | Log Levels
+
+```csharp
+// 调试信息(开发环境)| Debug information (development environment)
+_logger.Debug("调试信息:变量值={Value} | Debug info: variable value={Value}", someValue);
+
+// 一般信息 | General information
+_logger.Info("操作成功 | Operation successful");
+
+// 警告信息 | Warning information
+_logger.Warn("连接不稳定 | Connection unstable");
+
+// 错误信息(带异常)| Error information (with exception)
+_logger.Error(ex, "操作失败:{Message} | Operation failed: {Message}", ex.Message);
+
+// 致命错误 | Fatal error
+_logger.Fatal(ex, "系统崩溃 | System crash");
+```
+
+## 日志输出格式 | Log Output Format
+
+使用 `ForModule()` 后,日志会自动包含完整的类型信息:
+
+```
+2026-03-12 10:30:15.123 [INF] [XP.Hardware.Plc.Services.PlcService] 正在初始化 PLC 连接... | Initializing PLC connection...
+2026-03-12 10:30:16.456 [INF] [XP.Hardware.Plc.Services.PlcService] PLC 连接成功 | PLC connection successful
+```
+
+## 最佳实践 | Best Practices
+
+### 1. 在构造函数中初始化日志器 | Initialize Logger in Constructor
+
+```csharp
+public class MyService
+{
+ private readonly ILoggerService _logger;
+
+ public MyService(ILoggerService logger)
+ {
+ _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger));
+ }
+}
+```
+
+### 2. 使用结构化日志 | Use Structured Logging
+
+```csharp
+// 好的做法:使用占位符 | Good: use placeholders
+_logger.Info("用户 {UserId} 执行了操作 {Action} | User {UserId} performed action {Action}", userId, action);
+
+// 不好的做法:字符串拼接 | Bad: string concatenation
+_logger.Info($"用户 {userId} 执行了操作 {action}");
+```
+
+### 3. 异常日志包含上下文 | Exception Logs Include Context
+
+```csharp
+try
+{
+ await _plcClient.ConnectAsync(config);
+}
+catch (PlcException ex)
+{
+ _logger.Error(ex, "PLC 连接失败:地址={Address}, 端口={Port} | PLC connection failed: address={Address}, port={Port}",
+ config.Address, config.Port);
+ throw;
+}
+```
+
+## 对比:三种方式的输出 | Comparison: Output of Three Methods
+
+```csharp
+// 方式 1:ForModule() - 完整类型名
+// Method 1: ForModule() - Full type name
+_logger = logger.ForModule();
+// 输出 | Output: [XP.Hardware.Plc.Services.PlcService]
+
+// 方式 2:ForModule("PlcService") - 自定义名称
+// Method 2: ForModule("PlcService") - Custom name
+_logger = logger.ForModule("PlcService");
+// 输出 | Output: [PlcService]
+
+// 方式 3:不调用 ForModule - 无模块标记
+// Method 3: Don't call ForModule - No module tag
+_logger = logger;
+// 输出 | Output: [] (空标记 | empty tag)
+```
+
+## 配置 | Configuration
+
+日志配置在 `App.config` 中设置,通过 `SerilogConfig` 加载:
+
+```xml
+
+
+
+
+
+
+
+
+```
diff --git a/XP.Common/Documents/PdfViewer.README.md b/XP.Common/Documents/PdfViewer.README.md
new file mode 100644
index 0000000..04d5a05
--- /dev/null
+++ b/XP.Common/Documents/PdfViewer.README.md
@@ -0,0 +1,299 @@
+# PDF 查看与打印模块使用指南 | PDF Viewer & Printer Module Usage Guide
+
+## 概述 | Overview
+
+`XP.Common.PdfViewer` 提供基于 Telerik RadPdfViewer 的 PDF 文件查看与打印功能模块。模块作为 XP.Common 的通用可复用组件,通过 Prism DI 容器注册服务接口,供外部类库通过构造函数注入使用。
+
+### 核心功能 | Core Features
+
+- PDF 文件加载与显示(支持文件路径和文件流两种方式)
+- 内置 RadPdfViewerToolbar 提供页面导航、缩放、旋转、打印等完整工具栏 UI
+- 静默打印(指定打印机、页面范围、打印份数)
+- 打印设置对话框(用户交互式配置打印参数)
+- 打印预览功能
+- 多语言支持(简体中文、繁体中文、英文)
+- 结构化日志记录(使用 ILoggerService)
+- 资源自动释放(IDisposable + 终结器安全网)
+
+---
+
+## 目录结构 | Directory Structure
+
+```
+XP.Common/PdfViewer/
+├── Exceptions/ # 自定义异常
+│ ├── PdfLoadException.cs # PDF 加载异常
+│ ├── PrinterNotFoundException.cs # 打印机未找到异常
+│ └── PrintException.cs # 打印异常
+├── Interfaces/ # 服务接口
+│ ├── IPdfViewerService.cs # PDF 查看服务接口
+│ └── IPdfPrintService.cs # PDF 打印服务接口
+├── Implementations/ # 服务实现
+│ ├── PdfViewerService.cs # PdfViewerService 实现
+│ └── PdfPrintService.cs # PdfPrintService 实现
+├── ViewModels/ # ViewModel
+│ └── PdfViewerWindowViewModel.cs # 阅读器窗口 ViewModel
+└── Views/ # 视图
+ ├── PdfViewerWindow.xaml # 阅读器窗口 XAML
+ └── PdfViewerWindow.xaml.cs # 阅读器窗口 Code-Behind
+```
+
+---
+
+## 服务接口 | Service Interfaces
+
+### IPdfViewerService - PDF 查看服务
+
+负责 PDF 文件加载和阅读器窗口管理。
+
+```csharp
+public interface IPdfViewerService : IDisposable
+{
+ /// 通过文件路径打开 PDF 阅读器窗口
+ void OpenViewer(string filePath);
+
+ /// 通过文件流打开 PDF 阅读器窗口
+ void OpenViewer(Stream stream, string? title = null);
+}
+```
+
+### IPdfPrintService - PDF 打印服务
+
+负责 PDF 打印功能,包括静默打印和交互式打印。
+
+```csharp
+public interface IPdfPrintService
+{
+ /// 使用指定打印机打印 PDF 文件
+ void Print(string filePath, string printerName, int? pageFrom = null, int? pageTo = null, int copies = 1);
+
+ /// 打开打印设置对话框并打印
+ bool PrintWithDialog(string filePath);
+
+ /// 打开打印预览对话框
+ void PrintPreview(string filePath);
+}
+```
+
+---
+
+## 使用示例 | Usage Examples
+
+### 1. 通过文件路径打开 PDF
+
+```csharp
+using XP.Common.PdfViewer.Interfaces;
+
+public class MyService
+{
+ private readonly IPdfViewerService _pdfViewerService;
+
+ public MyService(IPdfViewerService pdfViewerService)
+ {
+ _pdfViewerService = pdfViewerService;
+ }
+
+ public void OpenPdfByPath()
+ {
+ // 打开指定路径的 PDF 文件
+ _pdfViewerService.OpenViewer(@"C:\Documents\UserManual.pdf");
+ }
+}
+```
+
+### 2. 通过文件流打开 PDF
+
+```csharp
+public void OpenPdfByStream()
+{
+ // 从文件流打开 PDF(窗口标题可选)
+ using var stream = File.OpenRead(@"C:\Documents\UserManual.pdf");
+ _pdfViewerService.OpenViewer(stream, "用户手册.pdf");
+}
+```
+
+### 3. 静默打印到指定打印机
+
+```csharp
+using XP.Common.PdfViewer.Interfaces;
+
+public class MyService
+{
+ private readonly IPdfPrintService _printService;
+
+ public MyService(IPdfPrintService printService)
+ {
+ _printService = printService;
+ }
+
+ public void PrintPdf()
+ {
+ // 打印全部页面到指定打印机
+ _printService.Print(
+ filePath: @"C:\Documents\UserManual.pdf",
+ printerName: "HP LaserJet Pro",
+ pageFrom: null, // null 表示从第一页
+ pageTo: null, // null 表示到最后一页
+ copies: 1 // 打印 1 份
+ );
+
+ // 打印指定范围(第 1-3 页)
+ _printService.Print(
+ filePath: @"C:\Documents\UserManual.pdf",
+ printerName: "HP LaserJet Pro",
+ pageFrom: 1,
+ pageTo: 3,
+ copies: 2
+ );
+ }
+}
+```
+
+### 4. 打开打印设置对话框
+
+```csharp
+public void OpenPrintDialog()
+{
+ // 显示打印设置对话框,用户确认后打印
+ bool userConfirmed = _printService.PrintWithDialog(@"C:\Documents\UserManual.pdf");
+
+ if (userConfirmed)
+ {
+ // 用户点击了"确定"按钮
+ }
+ else
+ {
+ // 用户点击了"取消"按钮
+ }
+}
+```
+
+### 5. 打开打印预览
+
+```csharp
+public void ShowPrintPreview()
+{
+ // 打开打印预览对话框
+ _printService.PrintPreview(@"C:\Documents\UserManual.pdf");
+}
+```
+
+---
+
+## DI 注册 | DI Registration
+
+在 `CommonModule.RegisterTypes` 中已注册为单例服务:
+
+```csharp
+containerRegistry.RegisterSingleton();
+containerRegistry.RegisterSingleton();
+```
+
+在 ViewModel 或 Service 中通过构造函数注入使用:
+
+```csharp
+public class MyViewModel
+{
+ private readonly IPdfViewerService _pdfViewerService;
+ private readonly IPdfPrintService _printService;
+
+ public MyViewModel(
+ IPdfViewerService pdfViewerService,
+ IPdfPrintService printService)
+ {
+ _pdfViewerService = pdfViewerService;
+ _printService = printService;
+ }
+}
+```
+
+---
+
+## 多语言资源 | Localization Resources
+
+PDF 模块支持多语言,资源键如下:
+
+| 资源键 | zh-CN | zh-TW | en-US |
+|--------|-------|-------|-------|
+| `PdfViewer_Title` | PDF 阅读器 | PDF 閱讀器 | PDF Viewer |
+| `PdfViewer_TitleWithFile` | PDF 阅读器 - {0} | PDF 閱讀器 - {0} | PDF Viewer - {0} |
+| `PdfViewer_LoadSuccess` | PDF 文件加载成功:{0}({1} 页)| PDF 檔案載入成功:{0}({1} 頁)| PDF loaded: {0} ({1} pages) |
+| `PdfViewer_LoadFailed` | PDF 文件加载失败 | PDF 檔案載入失敗 | PDF file load failed |
+| `PdfViewer_PrintSuccess` | 打印任务已提交:{0} → {1} | 列印任務已提交:{0} → {1} | Print job submitted: {0} → {1} |
+| `PdfViewer_PrintFailed` | 打印失败 | 列印失敗 | Print failed |
+| `PdfViewer_PrinterNotFound` | 打印机未找到:{0} | 印表機未找到:{0} | Printer not found: {0} |
+
+---
+
+## 异常处理 | Exception Handling
+
+| 异常类型 | 触发条件 | 说明 |
+|---------|---------|------|
+| `FileNotFoundException` | 文件路径不存在 | `OpenViewer(filePath)` 或 `Print(filePath, ...)` |
+| `ArgumentNullException` | 流参数为 null | `OpenViewer(null, ...)` |
+| `PdfLoadException` | PDF 格式无效或加载失败 | 文件损坏、非 PDF 格式等 |
+| `PrinterNotFoundException` | 指定打印机不存在 | `Print(filePath, printerName, ...)` |
+| `PrintException` | 打印过程中发生错误 | 打印机错误、驱动问题等 |
+
+---
+
+## 注意事项 | Notes
+
+1. **RadPdfViewerToolbar 内置功能**:页面导航(首页/上一页/下一页/末页)、缩放(放大/缩小/适合宽度/适合整页)、旋转(顺时针/逆时针)等功能由 RadPdfViewerToolbar 自动提供,无需手动实现。
+
+2. **资源释放**:`PdfViewerService` 实现 `IDisposable`,窗口��闭时会自动释放 PDF 文档资源。终结器作为安全网,确保未显式释放时也能清理资源。
+
+3. **多语言支持**:RadPdfViewerToolbar 的内置按钮文本(如"首页"、"上一页"、"放大"等)由 Telerik 自身的本地化机制管理,无需在 XP.Common 的 Resources 中维护。
+
+4. **打印设置**:Telerik 提供内置的 `PrintSettings` 类,无需自定义打印设置模型。
+
+5. **日志记录**:所有关键操作(加载成功/失败、打印成功/失败)都会通过 `ILoggerService` 记录结构化日志。
+
+---
+
+## 典型应用场景 | Typical Use Cases
+
+### 场景 1:主窗口添加"用户手册"按钮
+
+```csharp
+// MainWindowViewModel.cs
+private void ExecuteOpenUserManual()
+{
+ var manualPath = ConfigurationManager.AppSettings["UserManual"];
+ var stream = File.OpenRead(manualPath);
+ var fileName = Path.GetFileName(manualPath);
+ _pdfViewerService.OpenViewer(stream, fileName);
+}
+```
+
+### 场景 2:导出报告后自动打开 PDF 预览
+
+```csharp
+public void ExportAndPreview()
+{
+ // 生成 PDF 报告到临时文件
+ var tempPath = Path.Combine(Path.GetTempPath(), $"Report_{DateTime.Now:yyyyMMdd_HHmmss}.pdf");
+ GenerateReport(tempPath);
+
+ // 自动打开 PDF 预览
+ _pdfViewerService.OpenViewer(tempPath);
+}
+```
+
+### 场景 3:批量打印检测报告
+
+```csharp
+public void BatchPrintReports(List reportPaths, string printerName)
+{
+ foreach (var path in reportPaths)
+ {
+ _printService.Print(
+ filePath: path,
+ printerName: printerName,
+ pageFrom: 1,
+ pageTo: null,
+ copies: 1
+ );
+ }
+}
+```
diff --git a/XP.Common/Documents/RealTimeLogViewer.Design.md b/XP.Common/Documents/RealTimeLogViewer.Design.md
new file mode 100644
index 0000000..8bbdd15
--- /dev/null
+++ b/XP.Common/Documents/RealTimeLogViewer.Design.md
@@ -0,0 +1,50 @@
+# 实时日志查看器设计文档
+
+#### 1. 核心目标
+
+构建一个模态或非模态的 WPF 窗口,用于实时订阅 Serilog 事件,根据预定义的格式(如包含 `@l` 级别标记)自动渲染颜色,并提供过滤与自动滚动控制。
+
+---
+
+#### 2. 功能模块规划
+
+为了保证代码的可维护性和复用性,建议采用 MVVM 模式,参考 `XP.Common.GeneralForm` 的目录结构进行扩展:
+
+text
+
+编辑
+
+```
+XP.Common/
+└── GeneralForm/
+ └── Views/
+ ├── RealTimeLogViewer.xaml // 视图:RichTextBox + 控制栏
+ └── RealTimeLogViewer.xaml.cs
+ └── ViewModels/
+ └── RealTimeLogViewerViewModel.cs // 核心逻辑:处理颜色标记、过滤、滚动
+```
+
+#### 3. 详细功能规格
+
+| 功能模块 | 详细描述 | 交互逻辑 |
+| ------ | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
+| 颜色渲染引擎 | 基于 Serilog 标记
解析日志事件中的 Level 或 Message Template。 | * Error/Fatal: 红色/深红色
* Warning: 橙色/黄色
* Info: 默认色
* Debug: 浅灰色
利用 `RichTextBox` 的 `TextRange` 动态追加。 |
+| 动态过滤器 | 关键词黑白名单 | * 包含: 仅显示包含关键字的行。
* 排除: 隐藏包含关键字的行。
* 重置: 恢复显示所有。 |
+| 智能滚动 | 跟随开关 | * 开启: 新日志到达时,滚动条自动到底。
* 关闭: 用户可自由浏览历史,新日志仅在后台缓存/计数。 |
+| 行数限制 | 内存保护机制 | * 配置: 默认 2000 行,可调。
* 清理: 达到上限后,自动移除最旧的文本段落(`Paragraph`)。 |
+| 日志源接入 | 线程安全订阅 | 使用 `IObserver` 或 `IObservable`,内部通过 `Dispatcher` 安全更新。 |
+
+#### 4. 视觉交互 (UI/UX)
+
+- **布局**:
+ - **顶部工具栏**:包含“自动滚动”开关(带图标)、“清空日志”按钮、过滤输入框。
+ - **中部显示区**:`RichTextBox` (只读),启用垂直滚动条。
+ - **底部状态栏**:显示当前总日志条数、过滤后的条数。
+- **性能优化**:
+ - 由于 WPF `RichTextBox` 在大量文本下性能较差,建议增加**最大行数限制**(如默认保留 4000 行,超出自动删除顶部旧日志)。
+
+#### 6. 开发建议
+
+1. **颜色解析**:如果 Serilog 输出的是纯文本(如 `[INF] User logged in`),你需要编写正则表达式来匹配 `[INF]`、`[ERR]` 等前缀并着色。
+2. **线程安全**:务必参考 `ProgressWindow` 中 `UpdateProgress` 的实现,使用 `Dispatcher.InvokeAsync` 或 `Dispatcher.CheckAccess` 来确保从后台线程(如 Serilog 的异步写入线程)更新 UI 时不会崩溃。
+3. **资源占用**:考虑到这是一个“基础设施”类库,建议在窗口关闭时(`OnClosed` 事件)取消对 `IObservable` 的订阅,防止内存泄漏。
diff --git a/XP.Common/Dump/Configs/DumpConfig.cs b/XP.Common/Dump/Configs/DumpConfig.cs
new file mode 100644
index 0000000..497097e
--- /dev/null
+++ b/XP.Common/Dump/Configs/DumpConfig.cs
@@ -0,0 +1,34 @@
+namespace XP.Common.Dump.Configs
+{
+ ///
+ /// Dump 功能配置实体(从 App.config 读取)
+ /// Dump feature configuration entity (loaded from App.config)
+ ///
+ public class DumpConfig
+ {
+ ///
+ /// Dump 文件存储路径 | Dump file storage path
+ ///
+ public string StoragePath { get; set; } = @"D:\XplorePlane\Dump";
+
+ ///
+ /// 是否启用定时触发 | Enable scheduled trigger
+ ///
+ public bool EnableScheduledDump { get; set; } = false;
+
+ ///
+ /// 定时触发间隔(分钟)| Scheduled trigger interval (minutes)
+ ///
+ public int ScheduledIntervalMinutes { get; set; } = 60;
+
+ ///
+ /// Mini Dump 文件大小上限(MB)| Mini Dump file size limit (MB)
+ ///
+ public long MiniDumpSizeLimitMB { get; set; } = 100;
+
+ ///
+ /// 文件保留天数 | File retention days
+ ///
+ public int RetentionDays { get; set; } = 7;
+ }
+}
diff --git a/XP.Common/Dump/Configs/DumpTriggerType.cs b/XP.Common/Dump/Configs/DumpTriggerType.cs
new file mode 100644
index 0000000..4427c17
--- /dev/null
+++ b/XP.Common/Dump/Configs/DumpTriggerType.cs
@@ -0,0 +1,23 @@
+namespace XP.Common.Dump.Configs
+{
+ ///
+ /// Dump 触发类型 | Dump trigger type
+ ///
+ public enum DumpTriggerType
+ {
+ ///
+ /// 崩溃触发 | Crash trigger
+ ///
+ Crash,
+
+ ///
+ /// 定时触发 | Scheduled trigger
+ ///
+ Scheduled,
+
+ ///
+ /// 手动触发 | Manual trigger
+ ///
+ Manual
+ }
+}
diff --git a/XP.Common/Dump/Implementations/DumpCleaner.cs b/XP.Common/Dump/Implementations/DumpCleaner.cs
new file mode 100644
index 0000000..bf4b86e
--- /dev/null
+++ b/XP.Common/Dump/Implementations/DumpCleaner.cs
@@ -0,0 +1,69 @@
+using System;
+using System.IO;
+using System.Linq;
+using XP.Common.Dump.Configs;
+using XP.Common.Logging.Interfaces;
+
+namespace XP.Common.Dump.Implementations
+{
+ ///
+ /// Dump 文件自动清理组件,按保留天数策略删除过期文件
+ /// Auto cleanup component for dump files, deletes expired files based on retention policy
+ ///
+ internal class DumpCleaner
+ {
+ private readonly DumpConfig _config;
+ private readonly ILoggerService _logger;
+
+ public DumpCleaner(DumpConfig config, ILoggerService logger)
+ {
+ _config = config ?? throw new ArgumentNullException(nameof(config));
+ _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ ///
+ /// 执行清理操作:删除超过保留天数的 .dmp 文件
+ /// Execute cleanup: delete .dmp files older than retention days
+ ///
+ public void CleanExpiredFiles()
+ {
+ try
+ {
+ if (!Directory.Exists(_config.StoragePath))
+ {
+ _logger.Debug("存储目录不存在,跳过清理:{Path} | Storage directory does not exist, skipping cleanup: {Path}", _config.StoragePath);
+ return;
+ }
+
+ var dmpFiles = Directory.GetFiles(_config.StoragePath, "*.dmp");
+ var cutoffTime = DateTime.Now.AddDays(-_config.RetentionDays);
+ var deletedCount = 0;
+
+ foreach (var filePath in dmpFiles)
+ {
+ try
+ {
+ var creationTime = File.GetCreationTime(filePath);
+ if (creationTime < cutoffTime)
+ {
+ var fileName = Path.GetFileName(filePath);
+ File.Delete(filePath);
+ deletedCount++;
+ _logger.Info("已删除过期 Dump 文件:{FileName} | Deleted expired dump file: {FileName}", fileName);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "删除 Dump 文件失败:{FilePath} | Failed to delete dump file: {FilePath}", filePath);
+ }
+ }
+
+ _logger.Info("Dump 清理完成,共删除 {Count} 个文件 | Dump cleanup completed, {Count} files deleted", deletedCount);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "Dump 清理过程发生异常:{Message} | Dump cleanup error: {Message}", ex.Message);
+ }
+ }
+ }
+}
diff --git a/XP.Common/Dump/Implementations/DumpService.cs b/XP.Common/Dump/Implementations/DumpService.cs
new file mode 100644
index 0000000..6de5c9d
--- /dev/null
+++ b/XP.Common/Dump/Implementations/DumpService.cs
@@ -0,0 +1,321 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+using XP.Common.Dump.Configs;
+using XP.Common.Dump.Interfaces;
+using XP.Common.Dump.Native;
+using XP.Common.Logging.Interfaces;
+
+namespace XP.Common.Dump.Implementations
+{
+ ///
+ /// Dump 文件管理服务实现,负责 Dump 生成、崩溃事件订阅、定时调度和清理
+ /// Dump file management service implementation, handles dump generation, crash event subscription, scheduled tasks and cleanup
+ ///
+ public class DumpService : IDumpService
+ {
+ private readonly DumpConfig _config;
+ private readonly ILoggerService _logger;
+ private readonly DumpCleaner _cleaner;
+ private Timer? _scheduledTimer;
+ private Timer? _cleanupTimer;
+ private bool _isStarted;
+ private bool _disposed;
+
+ ///
+ /// 默认存储路径 | Default storage path
+ ///
+ private const string DefaultStoragePath = @"D:\XplorePlane\Dump";
+
+ public DumpService(DumpConfig config, ILoggerService logger)
+ {
+ _config = config ?? throw new ArgumentNullException(nameof(config));
+ _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger));
+ _cleaner = new DumpCleaner(config, logger);
+ }
+
+ ///
+ /// 生成 Dump 文件名(纯函数)| Generate dump file name (pure function)
+ /// 格式:XplorePlane_{yyyyMMdd_HHmm}_{TriggerType}.dmp
+ ///
+ /// 触发类型 | Trigger type
+ /// 时间戳 | Timestamp
+ /// 文件名 | File name
+ internal static string GenerateFileName(DumpTriggerType triggerType, DateTime timestamp)
+ {
+ return $"XplorePlane_{timestamp:yyyyMMdd_HHmm}_{triggerType}.dmp";
+ }
+
+ ///
+ /// 验证 Mini Dump 文件大小是否在限制范围内 | Validate Mini Dump file size is within limit
+ ///
+ /// 文件大小(字节)| File size in bytes
+ /// 大小限制(MB)| Size limit in MB
+ /// true 表示在限制内 | true if within limit
+ internal static bool ValidateFileSize(long fileSizeBytes, long sizeLimitMB)
+ {
+ return fileSizeBytes <= sizeLimitMB * 1024 * 1024;
+ }
+
+ ///
+ /// 写入 Dump 文件的核心方法 | Core method to write dump file
+ ///
+ /// 触发类型 | Trigger type
+ /// 是否为 Mini Dump | Whether it is a Mini Dump
+ /// 生成的文件完整路径,失败返回 null | Full path of generated file, null on failure
+ private string? WriteDump(DumpTriggerType triggerType, bool isMiniDump)
+ {
+ try
+ {
+ // 非手动触发请求 Full Dump 时,降级为 Mini Dump | Downgrade to Mini Dump for non-manual Full Dump requests
+ if (!isMiniDump && triggerType != DumpTriggerType.Manual)
+ {
+ _logger.Warn("非手动触发不允许生成 Full Dump,已降级为 Mini Dump:{TriggerType} | Non-manual trigger cannot generate Full Dump, downgraded to Mini Dump: {TriggerType}", triggerType);
+ isMiniDump = true;
+ }
+
+ var fileName = GenerateFileName(triggerType, DateTime.Now);
+ var filePath = Path.Combine(_config.StoragePath, fileName);
+
+ // 确定 Dump 类型标志 | Determine dump type flags
+ uint dumpType = isMiniDump
+ ? (uint)(MiniDumpType.MiniDumpWithDataSegs | MiniDumpType.MiniDumpWithHandleData | MiniDumpType.MiniDumpWithThreadInfo)
+ : (uint)MiniDumpType.MiniDumpWithFullMemory;
+
+ var process = Process.GetCurrentProcess();
+
+ using (var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None))
+ {
+ bool success = NativeMethods.MiniDumpWriteDump(
+ process.Handle,
+ (uint)process.Id,
+ fileStream.SafeFileHandle,
+ dumpType,
+ IntPtr.Zero,
+ IntPtr.Zero,
+ IntPtr.Zero);
+
+ if (!success)
+ {
+ var errorCode = Marshal.GetLastWin32Error();
+ _logger.Error(null!, "MiniDumpWriteDump 调用失败,错误码:{ErrorCode} | MiniDumpWriteDump failed, error code: {ErrorCode}", errorCode);
+ // 清理失败的文件 | Clean up failed file
+ try { File.Delete(filePath); } catch { }
+ return null;
+ }
+ }
+
+ // Mini Dump 文件大小检查 | Mini Dump file size check
+ if (isMiniDump)
+ {
+ var fileInfo = new FileInfo(filePath);
+ if (!ValidateFileSize(fileInfo.Length, _config.MiniDumpSizeLimitMB))
+ {
+ _logger.Warn("Mini Dump 文件超过大小限制({SizeMB}MB),已删除:{FilePath} | Mini Dump file exceeds size limit ({SizeMB}MB), deleted: {FilePath}",
+ _config.MiniDumpSizeLimitMB, filePath);
+ File.Delete(filePath);
+ return null;
+ }
+ }
+
+ var dumpTypeStr = isMiniDump ? "Mini" : "Full";
+ _logger.Info("Dump 文件已生成:{FilePath},触发类型:{TriggerType},Dump 类型:{DumpType} | Dump file generated: {FilePath}, trigger: {TriggerType}, type: {DumpType}",
+ filePath, triggerType, dumpTypeStr);
+
+ return filePath;
+ }
+ catch (IOException ex)
+ {
+ _logger.Error(ex, "Dump 文件写入失败:{Message} | Dump file write failed: {Message}", ex.Message);
+ return null;
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ _logger.Error(ex, "Dump 文件写入权限不足:{Message} | Dump file write permission denied: {Message}", ex.Message);
+ return null;
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "Dump 文件生成异常:{Message} | Dump file generation error: {Message}", ex.Message);
+ return null;
+ }
+ }
+
+ #region 手动触发方法 | Manual trigger methods
+
+ ///
+ public string? CreateMiniDump()
+ {
+ _logger.Info("手动触发 Mini Dump 生成 | Manually triggering Mini Dump generation");
+ var filePath = WriteDump(DumpTriggerType.Manual, isMiniDump: true);
+ if (filePath != null)
+ {
+ _logger.Info("手动 Mini Dump 已生成:{FilePath} | Manual Mini Dump generated: {FilePath}", filePath);
+ }
+ return filePath;
+ }
+
+ ///
+ public string? CreateFullDump()
+ {
+ _logger.Info("手动触发 Full Dump 生成 | Manually triggering Full Dump generation");
+ var filePath = WriteDump(DumpTriggerType.Manual, isMiniDump: false);
+ if (filePath != null)
+ {
+ _logger.Info("手动 Full Dump 已生成:{FilePath} | Manual Full Dump generated: {FilePath}", filePath);
+ }
+ return filePath;
+ }
+
+ #endregion
+
+ #region 生命周期管理 | Lifecycle management
+
+ ///
+ public void Start()
+ {
+ if (_isStarted) return;
+
+ _logger.Info("Dump 服务启动中 | Dump service starting");
+
+ // 确保存储目录存在 | Ensure storage directory exists
+ EnsureStorageDirectory();
+
+ // 订阅崩溃事件 | Subscribe to crash events
+ AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
+ TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
+
+ // 启动定时 Dump 计时器 | Start scheduled dump timer
+ if (_config.EnableScheduledDump)
+ {
+ var intervalMs = _config.ScheduledIntervalMinutes * 60 * 1000;
+ _scheduledTimer = new Timer(OnScheduledDumpCallback, null, intervalMs, intervalMs);
+ _logger.Info("定时 Dump 已启用,间隔:{Interval} 分钟 | Scheduled dump enabled, interval: {Interval} minutes", _config.ScheduledIntervalMinutes);
+ }
+
+ // 启动每日清理计时器(每24小时执行一次)| Start daily cleanup timer (every 24 hours)
+ var dailyMs = 24 * 60 * 60 * 1000;
+ _cleanupTimer = new Timer(OnCleanupCallback, null, dailyMs, dailyMs);
+
+ // 立即执行一次清理 | Execute cleanup immediately
+ _cleaner.CleanExpiredFiles();
+
+ _isStarted = true;
+ _logger.Info("Dump 服务已启动 | Dump service started");
+ }
+
+ ///
+ public void Stop()
+ {
+ if (!_isStarted) return;
+
+ _logger.Info("Dump 服务停止中 | Dump service stopping");
+
+ // 取消事件订阅 | Unsubscribe from events
+ AppDomain.CurrentDomain.UnhandledException -= OnUnhandledException;
+ TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException;
+
+ // 停止所有计时器 | Stop all timers
+ _scheduledTimer?.Change(Timeout.Infinite, Timeout.Infinite);
+ _cleanupTimer?.Change(Timeout.Infinite, Timeout.Infinite);
+
+ _isStarted = false;
+ _logger.Info("Dump 服务已停止 | Dump service stopped");
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (_disposed) return;
+
+ Stop();
+
+ _scheduledTimer?.Dispose();
+ _scheduledTimer = null;
+
+ _cleanupTimer?.Dispose();
+ _cleanupTimer = null;
+
+ _disposed = true;
+ }
+
+ #endregion
+
+ #region 内部方法 | Internal methods
+
+ ///
+ /// 确保存储目录存在,不存在则创建,创建失败回退默认路径
+ /// Ensure storage directory exists, create if not, fallback to default on failure
+ ///
+ private void EnsureStorageDirectory()
+ {
+ try
+ {
+ if (!Directory.Exists(_config.StoragePath))
+ {
+ Directory.CreateDirectory(_config.StoragePath);
+ _logger.Info("已创建存储目录:{Path} | Created storage directory: {Path}", _config.StoragePath);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "存储目录创建失败:{Path},回退到默认路径 | Storage directory creation failed: {Path}, falling back to default", _config.StoragePath);
+ _config.StoragePath = DefaultStoragePath;
+
+ try
+ {
+ if (!Directory.Exists(DefaultStoragePath))
+ {
+ Directory.CreateDirectory(DefaultStoragePath);
+ _logger.Info("已创建默认存储目录:{Path} | Created default storage directory: {Path}", DefaultStoragePath);
+ }
+ }
+ catch (Exception fallbackEx)
+ {
+ _logger.Fatal(fallbackEx, "默认存储目录创建也失败:{Path} | Default storage directory creation also failed: {Path}", DefaultStoragePath);
+ }
+ }
+ }
+
+ ///
+ /// 未处理异常回调 | Unhandled exception callback
+ ///
+ private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
+ {
+ _logger.Info("检测到未处理异常,正在生成崩溃 Dump | Unhandled exception detected, generating crash dump");
+ WriteDump(DumpTriggerType.Crash, isMiniDump: true);
+ }
+
+ ///
+ /// 未观察到的 Task 异常回调 | Unobserved task exception callback
+ ///
+ private void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
+ {
+ _logger.Info("检测到未观察的 Task 异常,正在生成崩溃 Dump | Unobserved task exception detected, generating crash dump");
+ WriteDump(DumpTriggerType.Crash, isMiniDump: true);
+ }
+
+ ///
+ /// 定时 Dump 回调 | Scheduled dump callback
+ ///
+ private void OnScheduledDumpCallback(object? state)
+ {
+ _logger.Debug("定时 Dump 触发 | Scheduled dump triggered");
+ WriteDump(DumpTriggerType.Scheduled, isMiniDump: true);
+ }
+
+ ///
+ /// 清理回调 | Cleanup callback
+ ///
+ private void OnCleanupCallback(object? state)
+ {
+ _logger.Debug("定时清理触发 | Scheduled cleanup triggered");
+ _cleaner.CleanExpiredFiles();
+ }
+
+ #endregion
+ }
+}
diff --git a/XP.Common/Dump/Interfaces/IDumpService.cs b/XP.Common/Dump/Interfaces/IDumpService.cs
new file mode 100644
index 0000000..098a113
--- /dev/null
+++ b/XP.Common/Dump/Interfaces/IDumpService.cs
@@ -0,0 +1,32 @@
+using System;
+
+namespace XP.Common.Dump.Interfaces
+{
+ ///
+ /// Dump 文件管理服务接口 | Dump file management service interface
+ ///
+ public interface IDumpService : IDisposable
+ {
+ ///
+ /// 手动触发 Mini Dump 生成 | Manually trigger Mini Dump generation
+ ///
+ /// 生成的 Dump 文件完整路径,失败返回 null | Full path of generated dump file, null on failure
+ string? CreateMiniDump();
+
+ ///
+ /// 手动触发 Full Dump 生成 | Manually trigger Full Dump generation
+ ///
+ /// 生成的 Dump 文件完整路径,失败返回 null | Full path of generated dump file, null on failure
+ string? CreateFullDump();
+
+ ///
+ /// 启动服务(订阅崩溃事件、启动定时任务和清理任务)| Start service (subscribe crash events, start scheduled and cleanup tasks)
+ ///
+ void Start();
+
+ ///
+ /// 停止服务(取消定时任务、取消事件订阅)| Stop service (cancel scheduled tasks, unsubscribe events)
+ ///
+ void Stop();
+ }
+}
diff --git a/XP.Common/Dump/Native/MiniDumpType.cs b/XP.Common/Dump/Native/MiniDumpType.cs
new file mode 100644
index 0000000..f625f56
--- /dev/null
+++ b/XP.Common/Dump/Native/MiniDumpType.cs
@@ -0,0 +1,37 @@
+using System;
+
+namespace XP.Common.Dump.Native
+{
+ ///
+ /// MiniDump 类型标志,用于 MiniDumpWriteDump 的 dumpType 参数
+ /// MiniDump type flags for MiniDumpWriteDump dumpType parameter
+ ///
+ [Flags]
+ internal enum MiniDumpType : uint
+ {
+ ///
+ /// 仅包含基本信息 | Basic information only
+ ///
+ MiniDumpNormal = 0x00000000,
+
+ ///
+ /// 包含数据段信息 | Include data segment information
+ ///
+ MiniDumpWithDataSegs = 0x00000001,
+
+ ///
+ /// 包含完整内存信息(Full Dump 使用)| Include full memory (used for Full Dump)
+ ///
+ MiniDumpWithFullMemory = 0x00000002,
+
+ ///
+ /// 包含句柄信息 | Include handle information
+ ///
+ MiniDumpWithHandleData = 0x00000004,
+
+ ///
+ /// 包含线程信息 | Include thread information
+ ///
+ MiniDumpWithThreadInfo = 0x00001000,
+ }
+}
diff --git a/XP.Common/Dump/Native/NativeMethods.cs b/XP.Common/Dump/Native/NativeMethods.cs
new file mode 100644
index 0000000..2a1678c
--- /dev/null
+++ b/XP.Common/Dump/Native/NativeMethods.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Runtime.InteropServices;
+using Microsoft.Win32.SafeHandles;
+
+namespace XP.Common.Dump.Native
+{
+ ///
+ /// Windows 原生方法 P/Invoke 声明 | Windows native method P/Invoke declarations
+ ///
+ internal static class NativeMethods
+ {
+ ///
+ /// 将进程的 Mini Dump 写入指定文件 | Write a Mini Dump of the process to the specified file
+ ///
+ [DllImport("dbghelp.dll", SetLastError = true)]
+ internal static extern bool MiniDumpWriteDump(
+ IntPtr hProcess,
+ uint processId,
+ SafeHandle hFile,
+ uint dumpType,
+ IntPtr exceptionParam,
+ IntPtr userStreamParam,
+ IntPtr callbackParam);
+ }
+}
diff --git a/XP.Common/GeneralForm/ViewModels/InputDialogViewModel.cs b/XP.Common/GeneralForm/ViewModels/InputDialogViewModel.cs
new file mode 100644
index 0000000..9a0ede5
--- /dev/null
+++ b/XP.Common/GeneralForm/ViewModels/InputDialogViewModel.cs
@@ -0,0 +1,109 @@
+using System;
+using Prism.Mvvm;
+using XP.Common.Localization;
+
+namespace XP.Common.GeneralForm.ViewModels
+{
+ ///
+ /// 输入对话框 ViewModel | Input dialog ViewModel
+ ///
+ public class InputDialogViewModel : BindableBase
+ {
+ private readonly Func? _validate;
+ private string _inputText;
+ private string? _validationError;
+
+ ///
+ /// 窗口标题 | Window title
+ ///
+ public string Title { get; }
+
+ ///
+ /// 提示文本 | Prompt text
+ ///
+ public string Prompt { get; }
+
+ ///
+ /// 确定按钮文本(多语言)| OK button text (localized)
+ ///
+ public string OkText { get; }
+
+ ///
+ /// 取消按钮文本(多语言)| Cancel button text (localized)
+ ///
+ public string CancelText { get; }
+
+ ///
+ /// 用户输入的文本 | User input text
+ ///
+ public string InputText
+ {
+ get => _inputText;
+ set
+ {
+ if (SetProperty(ref _inputText, value))
+ {
+ // 输入变化时清除验证错误 | Clear validation error on input change
+ ValidationError = null;
+ }
+ }
+ }
+
+ ///
+ /// 验证错误信息,null 表示无错误 | Validation error message, null means no error
+ ///
+ public string? ValidationError
+ {
+ get => _validationError;
+ set
+ {
+ if (SetProperty(ref _validationError, value))
+ {
+ RaisePropertyChanged(nameof(HasValidationError));
+ }
+ }
+ }
+
+ ///
+ /// 是否存在验证错误 | Whether there is a validation error
+ ///
+ public bool HasValidationError => !string.IsNullOrEmpty(ValidationError);
+
+ ///
+ /// 构造函数 | Constructor
+ ///
+ /// 提示文本 | Prompt text
+ /// 窗口标题 | Window title
+ /// 默认值 | Default value
+ /// 验证委托,返回 null 表示通过,返回错误信息则显示 | Validation delegate
+ public InputDialogViewModel(
+ string prompt,
+ string title,
+ string defaultValue = "",
+ Func? validate = null)
+ {
+ Prompt = prompt;
+ Title = title;
+ _inputText = defaultValue;
+ _validate = validate;
+
+ // 按钮文本使用多语言 | Button text uses localization
+ OkText = LocalizationHelper.Get("Button_OK");
+ CancelText = LocalizationHelper.Get("Button_Cancel");
+ }
+
+ ///
+ /// 执行验证,返回是否通过 | Run validation, returns whether it passed
+ ///
+ /// true 表示验证通过 | true means validation passed
+ public bool Validate()
+ {
+ if (_validate == null)
+ return true;
+
+ var error = _validate(InputText);
+ ValidationError = error;
+ return error == null;
+ }
+ }
+}
diff --git a/XP.Common/GeneralForm/ViewModels/ProgressWindowViewModel.cs b/XP.Common/GeneralForm/ViewModels/ProgressWindowViewModel.cs
new file mode 100644
index 0000000..652d714
--- /dev/null
+++ b/XP.Common/GeneralForm/ViewModels/ProgressWindowViewModel.cs
@@ -0,0 +1,103 @@
+using System;
+using Prism.Mvvm;
+using XP.Common.Logging.Interfaces;
+
+namespace XP.Common.GeneralForm.ViewModels
+{
+ ///
+ /// 进度条窗口 ViewModel,管理进度数据和窗口行为逻辑
+ ///
+ public class ProgressWindowViewModel : BindableBase
+ {
+ private readonly ILoggerService? _logger;
+ private string _message;
+ private double _progress;
+
+ ///
+ /// 窗口标题(只读)
+ ///
+ public string Title { get; }
+
+ ///
+ /// 提示信息文本
+ ///
+ public string Message
+ {
+ get => _message;
+ set => SetProperty(ref _message, value);
+ }
+
+ ///
+ /// 进度值,范围 0-100
+ ///
+ public double Progress
+ {
+ get => _progress;
+ set
+ {
+ if (SetProperty(ref _progress, value))
+ {
+ RaisePropertyChanged(nameof(ProgressText));
+ }
+ }
+ }
+
+ ///
+ /// 百分比显示文本,由 Progress 自动计算(只读)
+ ///
+ public string ProgressText => $"{Math.Floor(Progress)}%";
+
+ ///
+ /// 是否允许用户关闭窗口(只读)
+ ///
+ public bool IsCancelable { get; }
+
+ ///
+ /// 构造函数
+ ///
+ /// 窗口标题
+ /// 提示信息
+ /// 初始进度值
+ /// 是否允许取消
+ /// 日志服务(可选)
+ public ProgressWindowViewModel(
+ string title = "操作进行中",
+ string message = "请稍候...",
+ double progress = 0,
+ bool isCancelable = true,
+ ILoggerService? logger = null)
+ {
+ Title = title;
+ _message = message;
+ _progress = progress;
+ IsCancelable = isCancelable;
+
+ // 创建模块化日志实例(logger 为 null 时静默跳过)
+ _logger = logger?.ForModule();
+
+ // 构造时记录 Info 级别日志
+ _logger?.Info("进度窗口已创建:Title={Title}, IsCancelable={IsCancelable}", Title, IsCancelable);
+ }
+
+ ///
+ /// 更新进度和提示信息(唯一的外部更新入口)
+ ///
+ /// 提示信息文本
+ /// 进度值,超出 [0, 100] 范围时自动修正
+ public void UpdateProgress(string message, double progress)
+ {
+ // 检查 progress 是否超出有效范围,记录 Warn 日志
+ if (progress < 0 || progress > 100)
+ {
+ _logger?.Warn("进度值超出有效范围 [0, 100]:{Progress},将自动修正", progress);
+ }
+
+ // 将 progress 值 Clamp 到 [0, 100] 范围
+ progress = Math.Clamp(progress, 0, 100);
+
+ // 更新属性,触发 PropertyChanged 通知
+ Message = message;
+ Progress = progress;
+ }
+ }
+}
diff --git a/XP.Common/GeneralForm/Views/InputDialog.xaml b/XP.Common/GeneralForm/Views/InputDialog.xaml
new file mode 100644
index 0000000..c378162
--- /dev/null
+++ b/XP.Common/GeneralForm/Views/InputDialog.xaml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/XP.Common/GeneralForm/Views/InputDialog.xaml.cs b/XP.Common/GeneralForm/Views/InputDialog.xaml.cs
new file mode 100644
index 0000000..36bd289
--- /dev/null
+++ b/XP.Common/GeneralForm/Views/InputDialog.xaml.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Windows;
+using XP.Common.GeneralForm.ViewModels;
+using XP.Common.Localization;
+
+namespace XP.Common.GeneralForm.Views
+{
+ ///
+ /// 通用输入对话框,支持单行文本输入和可选的输入验证
+ /// General input dialog with single-line text input and optional validation
+ ///
+ public partial class InputDialog : Window
+ {
+ private readonly InputDialogViewModel _viewModel;
+
+ ///
+ /// 用户输入的结果,取消时为 null | User input result, null if cancelled
+ ///
+ public string? Result { get; private set; }
+
+ ///
+ /// 构造函数 | Constructor
+ ///
+ /// 提示文本 | Prompt text
+ /// 窗口标题 | Window title
+ /// 默认值 | Default value
+ /// 可选的验证委托,返回 null 表示通过,返回错误信息则阻止确认 | Optional validation delegate
+ public InputDialog(
+ string prompt,
+ string title,
+ string defaultValue = "",
+ Func? validate = null)
+ {
+ _viewModel = new InputDialogViewModel(prompt, title, defaultValue, validate);
+ DataContext = _viewModel;
+
+ InitializeComponent();
+
+ // 继承调用方窗口图标 | Inherit caller window icon
+ if (Application.Current?.MainWindow != null)
+ {
+ Icon = Application.Current.MainWindow.Icon;
+ }
+ }
+
+ ///
+ /// 确定按钮点击事件 | OK button click handler
+ ///
+ private void OnOkClick(object sender, RoutedEventArgs e)
+ {
+ // 执行验证 | Run validation
+ if (!_viewModel.Validate())
+ return;
+
+ Result = _viewModel.InputText;
+ DialogResult = true;
+ }
+
+ ///
+ /// 取消按钮点击事件 | Cancel button click handler
+ ///
+ private void OnCancelClick(object sender, RoutedEventArgs e)
+ {
+ Result = null;
+ DialogResult = false;
+ }
+
+ ///
+ /// 静态便捷方法,显示输入对话框并返回结果 | Static convenience method
+ ///
+ /// 提示文本 | Prompt text
+ /// 窗口标题 | Window title
+ /// 默认值 | Default value
+ /// 可选的验证委托 | Optional validation delegate
+ /// 父窗口(可选)| Owner window (optional)
+ /// 用户输入的值,取消时返回 null | User input, null if cancelled
+ public static string? Show(
+ string prompt,
+ string title,
+ string defaultValue = "",
+ Func? validate = null,
+ Window? owner = null)
+ {
+ var dialog = new InputDialog(prompt, title, defaultValue, validate);
+ // owner 未指定时自动回退到主窗口,确保对话框有父窗口约束不会被遮挡
+ // Fall back to MainWindow if owner not specified, ensuring dialog stays in front
+ dialog.Owner = owner ?? Application.Current?.MainWindow;
+ dialog.ShowDialog();
+ return dialog.Result;
+ }
+ }
+}
diff --git a/XP.Common/GeneralForm/Views/ProgressWindow.xaml b/XP.Common/GeneralForm/Views/ProgressWindow.xaml
new file mode 100644
index 0000000..db868cc
--- /dev/null
+++ b/XP.Common/GeneralForm/Views/ProgressWindow.xaml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/XP.Common/GeneralForm/Views/ProgressWindow.xaml.cs b/XP.Common/GeneralForm/Views/ProgressWindow.xaml.cs
new file mode 100644
index 0000000..70dbed8
--- /dev/null
+++ b/XP.Common/GeneralForm/Views/ProgressWindow.xaml.cs
@@ -0,0 +1,180 @@
+using System;
+using System.ComponentModel;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Interop;
+using XP.Common.GeneralForm.ViewModels;
+using XP.Common.Logging.Interfaces;
+
+namespace XP.Common.GeneralForm.Views
+{
+ ///
+ /// 通用进度条模态窗口,支持线程安全的进度更新和关闭操作
+ ///
+ public partial class ProgressWindow : Window
+ {
+ #region Win32 API
+
+ [DllImport("user32.dll")]
+ private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);
+
+ [DllImport("user32.dll")]
+ private static extern bool EnableMenuItem(IntPtr hMenu, uint uIDEnableItem, uint uEnable);
+
+ private const uint SC_CLOSE = 0xF060;
+ private const uint MF_BYCOMMAND = 0x00000000;
+ private const uint MF_GRAYED = 0x00000001;
+ private const uint MF_ENABLED = 0x00000000;
+
+ #endregion
+
+ private readonly ProgressWindowViewModel _viewModel;
+ private readonly ILoggerService? _logger;
+ private bool _isClosingByCode;
+
+ ///
+ /// 构造函数
+ ///
+ /// 窗口标题
+ /// 提示信息
+ /// 是否允许用户关闭窗口
+ /// 日志服务(可选)
+ public ProgressWindow(
+ string title = "操作进行中",
+ string message = "请稍候...",
+ bool isCancelable = true,
+ ILoggerService? logger = null)
+ {
+ // 创建 ViewModel 并设置为 DataContext
+ _viewModel = new ProgressWindowViewModel(title, message, 0, isCancelable, logger);
+ DataContext = _viewModel;
+
+ // 保存日志服务引用,用于 View 层日志记录
+ _logger = logger?.ForModule();
+
+ InitializeComponent();
+
+ // 继承主窗口图标
+ if (Application.Current?.MainWindow != null)
+ {
+ Icon = Application.Current.MainWindow.Icon;
+ }
+
+ // 订阅 Closing 事件,拦截用户手动关闭
+ Closing += OnWindowClosing;
+ }
+
+ ///
+ /// 更新进度和提示信息(线程安全,可从任意线程调用)
+ ///
+ /// 提示信息文本
+ /// 进度值
+ public void UpdateProgress(string message, double progress)
+ {
+ try
+ {
+ if (Dispatcher.CheckAccess())
+ {
+ _viewModel.UpdateProgress(message, progress);
+ }
+ else
+ {
+ Dispatcher.Invoke(() => _viewModel.UpdateProgress(message, progress));
+ }
+ }
+ catch (TaskCanceledException)
+ {
+ // 窗口已关闭时 Dispatcher 调度可能抛出此异常,静默处理
+ }
+ catch (Exception)
+ {
+ // 其他调度异常静默处理,避免影响调用方
+ }
+ }
+
+ ///
+ /// 线程安全关闭窗口(使用 new 关键字隐藏基类 Close 方法,因为 Window.Close() 不是虚方法)
+ ///
+ public new void Close()
+ {
+ try
+ {
+ _isClosingByCode = true;
+
+ if (Dispatcher.CheckAccess())
+ {
+ _logger?.Info("进度窗口正在关闭:Title={Title}", _viewModel.Title);
+ base.Close();
+ }
+ else
+ {
+ Dispatcher.Invoke(() =>
+ {
+ _logger?.Info("进度窗口正在关闭:Title={Title}", _viewModel.Title);
+ base.Close();
+ });
+ }
+ }
+ catch (TaskCanceledException)
+ {
+ // 窗口已关闭时 Dispatcher 调度可能抛出此异常,静默处理
+ }
+ catch (Exception)
+ {
+ // 其他调度异常静默处理,避免影响调用方
+ }
+ }
+
+ ///
+ /// 窗口源初始化时,根据 IsCancelable 控制关闭按钮状态
+ ///
+ protected override void OnSourceInitialized(EventArgs e)
+ {
+ base.OnSourceInitialized(e);
+
+ if (!_viewModel.IsCancelable)
+ {
+ DisableCloseButton();
+ }
+ }
+
+ ///
+ /// 拦截窗口关闭事件
+ ///
+ private void OnWindowClosing(object? sender, CancelEventArgs e)
+ {
+ // 当不可取消且非程序主动关闭时,阻止关闭
+ if (!_viewModel.IsCancelable && !_isClosingByCode)
+ {
+ e.Cancel = true;
+ }
+ }
+
+ ///
+ /// 通过 Win32 API 禁用窗口关闭按钮(灰色不可点击)
+ ///
+ private void DisableCloseButton()
+ {
+ var hwnd = new WindowInteropHelper(this).Handle;
+ var hMenu = GetSystemMenu(hwnd, false);
+ if (hMenu != IntPtr.Zero)
+ {
+ EnableMenuItem(hMenu, SC_CLOSE, MF_BYCOMMAND | MF_GRAYED);
+ }
+ }
+
+ ///
+ /// 通过 Win32 API 启用窗口关闭按钮
+ ///
+ private void EnableCloseButton()
+ {
+ var hwnd = new WindowInteropHelper(this).Handle;
+ var hMenu = GetSystemMenu(hwnd, false);
+ if (hMenu != IntPtr.Zero)
+ {
+ EnableMenuItem(hMenu, SC_CLOSE, MF_BYCOMMAND | MF_ENABLED);
+ }
+ }
+ }
+}
diff --git a/XP.Common/Helpers/ConfigLoader.cs b/XP.Common/Helpers/ConfigLoader.cs
new file mode 100644
index 0000000..4de6137
--- /dev/null
+++ b/XP.Common/Helpers/ConfigLoader.cs
@@ -0,0 +1,91 @@
+using System.Configuration;
+using XP.Common.Configs;
+using XP.Common.Dump.Configs;
+
+namespace XP.Common.Helpers
+{
+ ///
+ /// 通用配置加载工具(读取App.config)
+ ///
+ public static class ConfigLoader
+ {
+ ///
+ /// 加载Serilog配置
+ ///
+ public static SerilogConfig LoadSerilogConfig()
+ {
+ var config = new SerilogConfig();
+
+ var logPath = ConfigurationManager.AppSettings["Serilog:LogPath"];
+ if (!string.IsNullOrEmpty(logPath)) config.LogPath = logPath;
+
+ var minLevel = ConfigurationManager.AppSettings["Serilog:MinimumLevel"];
+ if (!string.IsNullOrEmpty(minLevel)) config.MinimumLevel = minLevel;
+
+ var enableConsole = ConfigurationManager.AppSettings["Serilog:EnableConsole"];
+ if (bool.TryParse(enableConsole, out var console)) config.EnableConsole = console;
+
+ var rollingInterval = ConfigurationManager.AppSettings["Serilog:RollingInterval"];
+ if (!string.IsNullOrEmpty(rollingInterval)) config.RollingInterval = rollingInterval;
+
+ var fileSize = ConfigurationManager.AppSettings["Serilog:FileSizeLimitMB"];
+ if (long.TryParse(fileSize, out var size)) config.FileSizeLimitMB = size;
+
+ var retainCount = ConfigurationManager.AppSettings["Serilog:RetainedFileCountLimit"];
+ if (int.TryParse(retainCount, out var count)) config.RetainedFileCountLimit = count;
+
+ return config;
+ }
+
+ ///
+ /// 加载SQLite配置
+ ///
+ public static SqliteConfig LoadSqliteConfig()
+ {
+ var config = new SqliteConfig();
+
+ var dbPath = ConfigurationManager.AppSettings["Sqlite:DbFilePath"];
+ if (!string.IsNullOrEmpty(dbPath)) config.DbFilePath = dbPath;
+
+ var timeout = ConfigurationManager.AppSettings["Sqlite:ConnectionTimeout"];
+ if (int.TryParse(timeout, out var t)) config.ConnectionTimeout = t;
+
+ var createIfNotExists = ConfigurationManager.AppSettings["Sqlite:CreateIfNotExists"];
+ if (bool.TryParse(createIfNotExists, out var c)) config.CreateIfNotExists = c;
+
+ var enableWal = ConfigurationManager.AppSettings["Sqlite:EnableWalMode"];
+ if (bool.TryParse(enableWal, out var w)) config.EnableWalMode = w;
+
+ var enableSqlLog = ConfigurationManager.AppSettings["Sqlite:EnableSqlLogging"];
+ if (bool.TryParse(enableSqlLog, out var l)) config.EnableSqlLogging = l;
+
+ return config;
+ }
+
+ ///
+ /// 加载 Dump 配置 | Load Dump configuration
+ ///
+ public static DumpConfig LoadDumpConfig()
+ {
+ var config = new DumpConfig();
+
+ var storagePath = ConfigurationManager.AppSettings["Dump:StoragePath"];
+ if (!string.IsNullOrEmpty(storagePath)) config.StoragePath = storagePath;
+
+ var enableScheduled = ConfigurationManager.AppSettings["Dump:EnableScheduledDump"];
+ if (bool.TryParse(enableScheduled, out var enabled)) config.EnableScheduledDump = enabled;
+
+ var interval = ConfigurationManager.AppSettings["Dump:ScheduledIntervalMinutes"];
+ if (int.TryParse(interval, out var min)) config.ScheduledIntervalMinutes = min;
+
+ var sizeLimit = ConfigurationManager.AppSettings["Dump:MiniDumpSizeLimitMB"];
+ if (long.TryParse(sizeLimit, out var size)) config.MiniDumpSizeLimitMB = size;
+
+ var retentionDays = ConfigurationManager.AppSettings["Dump:RetentionDays"];
+ if (int.TryParse(retentionDays, out var days)) config.RetentionDays = days;
+
+ return config;
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/XP.Common/Helpers/ProcessHelper.cs b/XP.Common/Helpers/ProcessHelper.cs
new file mode 100644
index 0000000..34e6861
--- /dev/null
+++ b/XP.Common/Helpers/ProcessHelper.cs
@@ -0,0 +1,199 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace XP.Common.Helpers
+{
+ ///
+ /// 进程管理工具类 | Process Management Helper
+ /// 提供启动外部程序、窗口置前等通用功能 | Provides common functions like launching external programs and bringing windows to front
+ ///
+ public static class ProcessHelper
+ {
+ #region Win32 API
+
+ [DllImport("user32.dll")]
+ private static extern bool SetForegroundWindow(IntPtr hWnd);
+
+ [DllImport("user32.dll")]
+ private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
+
+ [DllImport("user32.dll")]
+ private static extern bool IsIconic(IntPtr hWnd);
+
+ private const int SW_RESTORE = 9;
+
+ #endregion
+
+ ///
+ /// 启动或激活外部程序 | Start or activate an external program
+ ///
+ /// 可执行文件完整路径 | Full path to the executable
+ /// 是否单实例模式:true=已运行则置前,false=直接启动新实例 | Single instance mode: true=activate if running, false=always start new instance
+ /// 操作结果 | Operation result
+ public static ProcessResult StartOrActivate(string exePath, bool singleInstance = true)
+ {
+ if (string.IsNullOrWhiteSpace(exePath))
+ {
+ return ProcessResult.Fail("程序路径未配置 | Program path not configured");
+ }
+
+ if (!File.Exists(exePath))
+ {
+ return ProcessResult.Fail($"程序文件不存在: {exePath} | Program file not found: {exePath}");
+ }
+
+ try
+ {
+ // 获取进程名(不含扩展名)| Get process name (without extension)
+ var processName = Path.GetFileNameWithoutExtension(exePath);
+
+ if (singleInstance)
+ {
+ var processes = Process.GetProcessesByName(processName);
+
+ if (processes.Length > 0)
+ {
+ // 程序已运行,将窗口置前 | Program already running, bring window to front
+ var process = processes[0];
+ var hWnd = process.MainWindowHandle;
+
+ if (hWnd != IntPtr.Zero)
+ {
+ BringToFront(hWnd);
+ }
+
+ return ProcessResult.Activated();
+ }
+ }
+
+ // 启动程序 | Start program
+ Process.Start(new ProcessStartInfo
+ {
+ FileName = exePath,
+ UseShellExecute = true
+ });
+
+ return ProcessResult.Started();
+ }
+ catch (Exception ex)
+ {
+ return ProcessResult.Fail($"操作失败: {ex.Message} | Operation failed: {ex.Message}");
+ }
+ }
+
+ ///
+ /// 判断指定进程是否正在运行 | Check if a process is running
+ ///
+ /// 进程名(不含扩展名)| Process name (without extension)
+ /// 是否运行中 | Whether the process is running
+ public static bool IsProcessRunning(string processName)
+ {
+ return Process.GetProcessesByName(processName).Length > 0;
+ }
+
+ ///
+ /// 根据可执行文件路径查找已运行的进程 | Find a running process by executable file path
+ /// 通过进程名匹配并验证完整路径,确保找到的是同一个程序 | Matches by process name and verifies full path
+ ///
+ /// 可执行文件完整路径 | Full path to the executable
+ /// 找到的进程对象,未找到返回 null | The found process, or null if not found
+ public static Process FindRunningProcess(string exePath)
+ {
+ if (string.IsNullOrWhiteSpace(exePath))
+ return null;
+
+ var processName = Path.GetFileNameWithoutExtension(exePath);
+ var normalizedTarget = NormalizePath(exePath);
+
+ try
+ {
+ var processes = Process.GetProcessesByName(processName);
+ foreach (var proc in processes)
+ {
+ try
+ {
+ // 验证完整路径匹配,避免同名不同路径的进程误关联
+ var procPath = proc.MainModule?.FileName;
+ if (procPath != null && string.Equals(NormalizePath(procPath), normalizedTarget, StringComparison.OrdinalIgnoreCase))
+ {
+ return proc;
+ }
+ }
+ catch
+ {
+ // 无法访问进程信息(权限不足等),跳过
+ }
+ }
+ }
+ catch
+ {
+ // GetProcessesByName 异常,返回 null
+ }
+
+ return null;
+ }
+
+ ///
+ /// 标准化文件路径,用于路径比较 | Normalize file path for comparison
+ ///
+ private static string NormalizePath(string path)
+ {
+ return Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+ }
+
+ ///
+ /// 将窗口置前显示 | Bring a window to the foreground
+ ///
+ /// 窗口句柄 | Window handle
+ public static void BringToFront(IntPtr hWnd)
+ {
+ if (hWnd == IntPtr.Zero) return;
+
+ // 如果窗口被最小化,先恢复 | If window is minimized, restore it first
+ if (IsIconic(hWnd))
+ {
+ ShowWindow(hWnd, SW_RESTORE);
+ }
+
+ SetForegroundWindow(hWnd);
+ }
+ }
+
+ ///
+ /// 进程操作结果 | Process Operation Result
+ ///
+ public class ProcessResult
+ {
+ ///
+ /// 是否成功 | Whether the operation succeeded
+ ///
+ public bool Success { get; private set; }
+
+ ///
+ /// 是否为新启动(false表示激活已有窗口)| Whether it was newly started (false means activated existing window)
+ ///
+ public bool IsNewlyStarted { get; private set; }
+
+ ///
+ /// 错误信息 | Error message
+ ///
+ public string ErrorMessage { get; private set; }
+
+ ///
+ /// 创建启动成功结果 | Create a started result
+ ///
+ public static ProcessResult Started() => new() { Success = true, IsNewlyStarted = true };
+
+ ///
+ /// 创建激活成功结果 | Create an activated result
+ ///
+ public static ProcessResult Activated() => new() { Success = true, IsNewlyStarted = false };
+
+ ///
+ /// 创建失败结果 | Create a failed result
+ ///
+ public static ProcessResult Fail(string errorMessage) => new() { Success = false, ErrorMessage = errorMessage };
+ }
+}
diff --git a/XP.Common/Localization/Configs/LocalizationConfig.cs b/XP.Common/Localization/Configs/LocalizationConfig.cs
new file mode 100644
index 0000000..a504b3a
--- /dev/null
+++ b/XP.Common/Localization/Configs/LocalizationConfig.cs
@@ -0,0 +1,142 @@
+using System;
+using System.Configuration;
+using System.Globalization;
+using XP.Common.Localization.Enums;
+using XP.Common.Localization.Interfaces;
+using XP.Common.Logging.Interfaces;
+
+namespace XP.Common.Localization.Configs
+{
+ ///
+ /// 本地化配置实现 | Localization configuration implementation
+ /// 使用 App.config 存储语言设置 | Uses App.config to store language settings
+ ///
+ public class LocalizationConfig : ILocalizationConfig
+ {
+ private const string LanguageKey = "Language";
+ private readonly ILoggerService _logger;
+
+ ///
+ /// 构造函数 | Constructor
+ ///
+ /// 日志服务 | Logger service
+ public LocalizationConfig(ILoggerService logger)
+ {
+ _logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule();
+ }
+
+ ///
+ /// 获取保存的语言设置 | Get saved language setting
+ ///
+ /// 语言设置,如果未设置则返回 null | Language setting, or null if not set
+ public SupportedLanguage? GetSavedLanguage()
+ {
+ try
+ {
+ var languageString = ConfigurationManager.AppSettings[LanguageKey];
+
+ if (string.IsNullOrWhiteSpace(languageString))
+ {
+ _logger.Info("No saved language setting found in configuration");
+ return null;
+ }
+
+ if (Enum.TryParse(languageString, true, out var language))
+ {
+ _logger.Info($"Loaded saved language setting: {language}");
+ return language;
+ }
+
+ _logger.Warn($"Invalid language setting in configuration: '{languageString}'. Expected values: ZhCN, ZhTW, EnUS");
+ return null;
+ }
+ catch (ConfigurationErrorsException ex)
+ {
+ _logger.Error(ex, "Configuration error while reading language setting");
+ return null;
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "Unexpected error while reading saved language setting");
+ return null;
+ }
+ }
+
+ ///
+ /// 保存语言设置 | Save language setting
+ ///
+ /// 要保存的语言 | Language to save
+ public void SaveLanguage(SupportedLanguage language)
+ {
+ try
+ {
+ var config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
+ var appSettings = config.AppSettings.Settings;
+
+ var languageValue = language.ToString();
+
+ if (appSettings[LanguageKey] == null)
+ {
+ appSettings.Add(LanguageKey, languageValue);
+ _logger.Info($"Added new language setting to configuration: {language}");
+ }
+ else
+ {
+ appSettings[LanguageKey].Value = languageValue;
+ _logger.Info($"Updated language setting in configuration: {language}");
+ }
+
+ config.Save(ConfigurationSaveMode.Modified);
+ ConfigurationManager.RefreshSection("appSettings");
+
+ _logger.Info($"Successfully saved language setting: {language}");
+ }
+ catch (ConfigurationErrorsException ex)
+ {
+ _logger.Error(ex, $"Configuration error while saving language setting: {language}");
+ throw new InvalidOperationException($"Failed to save language setting to configuration file: {ex.Message}", ex);
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ _logger.Error(ex, $"Access denied while saving language setting: {language}");
+ throw new InvalidOperationException("Access denied to configuration file. Please check file permissions.", ex);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, $"Unexpected error while saving language setting: {language}");
+ throw new InvalidOperationException($"Failed to save language setting: {ex.Message}", ex);
+ }
+ }
+
+ ///
+ /// 获取系统默认语言 | Get system default language
+ ///
+ /// 系统默认语言 | System default language
+ public SupportedLanguage GetSystemDefaultLanguage()
+ {
+ try
+ {
+ var systemCulture = CultureInfo.CurrentUICulture.Name;
+ _logger.Info($"Detecting system default language from culture: {systemCulture}");
+
+ var defaultLanguage = systemCulture switch
+ {
+ "zh-CN" => SupportedLanguage.ZhCN,
+ "zh-TW" or "zh-HK" or "zh-MO" => SupportedLanguage.ZhTW,
+ "en-US" or "en" => SupportedLanguage.EnUS,
+ _ when systemCulture.StartsWith("zh", StringComparison.OrdinalIgnoreCase) => SupportedLanguage.ZhCN,
+ _ when systemCulture.StartsWith("en", StringComparison.OrdinalIgnoreCase) => SupportedLanguage.EnUS,
+ _ => SupportedLanguage.ZhCN // 默认简体中文 | Default to Simplified Chinese
+ };
+
+ _logger.Info($"System default language determined: {defaultLanguage} (from culture: {systemCulture})");
+ return defaultLanguage;
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "Error while detecting system default language, falling back to ZhCN");
+ return SupportedLanguage.ZhCN; // 默认简体中文 | Default to Simplified Chinese
+ }
+ }
+ }
+}
diff --git a/XP.Common/Localization/Enums/SupportedLanguage.cs b/XP.Common/Localization/Enums/SupportedLanguage.cs
new file mode 100644
index 0000000..8d148bc
--- /dev/null
+++ b/XP.Common/Localization/Enums/SupportedLanguage.cs
@@ -0,0 +1,28 @@
+using System.ComponentModel;
+
+namespace XP.Common.Localization.Enums
+{
+ ///
+ /// 支持的语言枚举 | Supported language enumeration
+ ///
+ public enum SupportedLanguage
+ {
+ ///
+ /// 简体中文 | Simplified Chinese
+ ///
+ [Description("zh-CN")]
+ ZhCN,
+
+ ///
+ /// 繁体中文 | Traditional Chinese
+ ///
+ [Description("zh-TW")]
+ ZhTW,
+
+ ///
+ /// 美式英语 | American English
+ ///
+ [Description("en-US")]
+ EnUS
+ }
+}
diff --git a/XP.Common/Localization/Events/LanguageChangedEvent.cs b/XP.Common/Localization/Events/LanguageChangedEvent.cs
new file mode 100644
index 0000000..a707347
--- /dev/null
+++ b/XP.Common/Localization/Events/LanguageChangedEvent.cs
@@ -0,0 +1,12 @@
+using Prism.Events;
+
+namespace XP.Common.Localization.Events
+{
+ ///
+ /// Prism 语言切换事件 | Prism language changed event
+ /// 用于跨模块通知语言切换 | Used for cross-module language change notification
+ ///
+ public class LanguageChangedEvent : PubSubEvent
+ {
+ }
+}
diff --git a/XP.Common/Localization/Events/LanguageChangedEventArgs.cs b/XP.Common/Localization/Events/LanguageChangedEventArgs.cs
new file mode 100644
index 0000000..ced40d3
--- /dev/null
+++ b/XP.Common/Localization/Events/LanguageChangedEventArgs.cs
@@ -0,0 +1,38 @@
+using System;
+using XP.Common.Localization.Enums;
+
+namespace XP.Common.Localization.Events
+{
+ ///
+ /// 语言切换事件参数 | Language changed event arguments
+ ///
+ public class LanguageChangedEventArgs : EventArgs
+ {
+ ///
+ /// 旧语言 | Old language
+ ///
+ public SupportedLanguage OldLanguage { get; }
+
+ ///
+ /// 新语言 | New language
+ ///
+ public SupportedLanguage NewLanguage { get; }
+
+ ///
+ /// 切换时间 | Change timestamp
+ ///
+ public DateTime Timestamp { get; }
+
+ ///
+ /// 构造函数 | Constructor
+ ///
+ /// 旧语言 | Old language
+ /// 新语言 | New language
+ public LanguageChangedEventArgs(SupportedLanguage oldLanguage, SupportedLanguage newLanguage)
+ {
+ OldLanguage = oldLanguage;
+ NewLanguage = newLanguage;
+ Timestamp = DateTime.Now;
+ }
+ }
+}
diff --git a/XP.Common/Localization/Exceptions/LocalizationException.cs b/XP.Common/Localization/Exceptions/LocalizationException.cs
new file mode 100644
index 0000000..cc1df64
--- /dev/null
+++ b/XP.Common/Localization/Exceptions/LocalizationException.cs
@@ -0,0 +1,28 @@
+using System;
+
+namespace XP.Common.Localization.Exceptions
+{
+ ///
+ /// 本地化异常基类 | Localization exception base class
+ ///
+ public class LocalizationException : Exception
+ {
+ ///
+ /// 构造函数 | Constructor
+ ///
+ /// 异常消息 | Exception message
+ public LocalizationException(string message) : base(message)
+ {
+ }
+
+ ///
+ /// 构造函数 | Constructor
+ ///
+ /// 异常消息 | Exception message
+ /// 内部异常 | Inner exception
+ public LocalizationException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+ }
+}
diff --git a/XP.Common/Localization/Exceptions/LocalizationInitializationException.cs b/XP.Common/Localization/Exceptions/LocalizationInitializationException.cs
new file mode 100644
index 0000000..dd7801e
--- /dev/null
+++ b/XP.Common/Localization/Exceptions/LocalizationInitializationException.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace XP.Common.Localization.Exceptions
+{
+ ///
+ /// 本地化初始化异常 | Localization initialization exception
+ /// 当资源文件无法加载时抛出 | Thrown when resource files cannot be loaded
+ ///
+ public class LocalizationInitializationException : LocalizationException
+ {
+ ///
+ /// 构造函数 | Constructor
+ ///
+ /// 异常消息 | Exception message
+ public LocalizationInitializationException(string message)
+ : base(message)
+ {
+ }
+
+ ///
+ /// 构造函数 | Constructor
+ ///
+ /// 异常消息 | Exception message
+ /// 内部异常 | Inner exception
+ public LocalizationInitializationException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+ }
+}
diff --git a/XP.Common/Localization/Exceptions/ResourceKeyNotFoundException.cs b/XP.Common/Localization/Exceptions/ResourceKeyNotFoundException.cs
new file mode 100644
index 0000000..2d67a63
--- /dev/null
+++ b/XP.Common/Localization/Exceptions/ResourceKeyNotFoundException.cs
@@ -0,0 +1,25 @@
+using System;
+
+namespace XP.Common.Localization.Exceptions
+{
+ ///
+ /// 资源键未找到异常 | Resource key not found exception
+ ///
+ public class ResourceKeyNotFoundException : LocalizationException
+ {
+ ///
+ /// 资源键 | Resource key
+ ///
+ public string ResourceKey { get; }
+
+ ///
+ /// 构造函数 | Constructor
+ ///
+ /// 资源键 | Resource key
+ public ResourceKeyNotFoundException(string resourceKey)
+ : base($"Resource key not found: {resourceKey}")
+ {
+ ResourceKey = resourceKey;
+ }
+ }
+}
diff --git a/XP.Common/Localization/Extensions/LocalizationExtension.cs b/XP.Common/Localization/Extensions/LocalizationExtension.cs
new file mode 100644
index 0000000..943f46c
--- /dev/null
+++ b/XP.Common/Localization/Extensions/LocalizationExtension.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Windows.Markup;
+using XP.Common.Localization.Interfaces;
+
+namespace XP.Common.Localization.Extensions
+{
+ ///
+ /// 本地化标记扩展 | Localization markup extension
+ /// 用法: {loc:Localization Key=ResourceKey} | Usage: {loc:Localization Key=ResourceKey}
+ ///
+ [MarkupExtensionReturnType(typeof(string))]
+ public class LocalizationExtension : MarkupExtension
+ {
+ private static ILocalizationService? _localizationService;
+ private string _key = string.Empty;
+
+ ///
+ /// 资源键 | Resource key
+ ///
+ [ConstructorArgument("key")]
+ public string Key
+ {
+ get => _key;
+ set => _key = value;
+ }
+
+ ///
+ /// 默认构造函数 | Default constructor
+ ///
+ public LocalizationExtension()
+ {
+ }
+
+ ///
+ /// 带资源键的构造函数 | Constructor with resource key
+ ///
+ /// 资源键 | Resource key
+ public LocalizationExtension(string key)
+ {
+ _key = key;
+ }
+
+ ///
+ /// 初始化本地化服务(由 CommonModule 调用)| Initialize localization service (called by CommonModule)
+ ///
+ /// 本地化服务实例 | Localization service instance
+ public static void Initialize(ILocalizationService localizationService)
+ {
+ _localizationService = localizationService;
+ }
+
+ ///
+ /// 提供本地化字符串值 | Provide localized string value
+ ///
+ /// 服务提供者 | Service provider
+ /// 本地化字符串 | Localized string
+ public override object ProvideValue(IServiceProvider serviceProvider)
+ {
+ if (string.IsNullOrEmpty(_key))
+ {
+ return string.Empty;
+ }
+
+ if (_localizationService == null)
+ {
+ // 设计时回退 | Design-time fallback
+ return $"[{_key}]";
+ }
+
+ // 直接返回当前语言的翻译字符串 | Return translated text for current language
+ return _localizationService.GetString(_key);
+ }
+ }
+}
diff --git a/XP.Common/Localization/Helpers/LocalizationHelper.cs b/XP.Common/Localization/Helpers/LocalizationHelper.cs
new file mode 100644
index 0000000..7114b6d
--- /dev/null
+++ b/XP.Common/Localization/Helpers/LocalizationHelper.cs
@@ -0,0 +1,57 @@
+using System.Globalization;
+using System.Resources;
+using XP.Common.Localization.Interfaces;
+using XP.Common.Resources;
+
+namespace XP.Common.Localization
+{
+ ///
+ /// 静态本地化帮助类 | Static localization helper
+ /// 用法:Loc.Get("ResourceKey") 或 Loc.Get("ResourceKey", param1, param2)
+ /// Usage: Loc.Get("ResourceKey") or Loc.Get("ResourceKey", param1, param2)
+ ///
+ public static class LocalizationHelper
+ {
+ private static ILocalizationService? _localizationService;
+
+ ///
+ /// 初始化本地化帮助类(由 CommonModule 或 App 启动时调用)
+ /// Initialize the localization helper (called by CommonModule or App at startup)
+ ///
+ /// 本地化服务实例 | Localization service instance
+ public static void Initialize(ILocalizationService localizationService)
+ {
+ _localizationService = localizationService;
+ }
+
+ ///
+ /// 获取本地化字符串(使用当前 UI 语言)| Get localized string (using current UI culture)
+ ///
+ /// 资源键 | Resource key
+ /// 本地化字符串,找不到时返回键本身 | Localized string, returns key itself if not found
+ public static string Get(string key)
+ {
+ if (string.IsNullOrEmpty(key))
+ return string.Empty;
+
+ if (_localizationService != null)
+ return _localizationService.GetString(key);
+
+ // 兼容回退:未初始化时仍使用原始 ResourceManager
+ // Fallback: use original ResourceManager when not initialized
+ return Resources.Resources.ResourceManager.GetString(key, CultureInfo.CurrentUICulture) ?? key;
+ }
+
+ ///
+ /// 获取本地化字符串并格式化 | Get localized string with formatting
+ ///
+ /// 资源键 | Resource key
+ /// 格式化参数 | Format arguments
+ /// 格式化后的本地化字符串 | Formatted localized string
+ public static string Get(string key, params object[] args)
+ {
+ var template = Get(key);
+ return args.Length > 0 ? string.Format(template, args) : template;
+ }
+ }
+}
diff --git a/XP.Common/Localization/Implementations/ResourceSource.cs b/XP.Common/Localization/Implementations/ResourceSource.cs
new file mode 100644
index 0000000..2d1d0cf
--- /dev/null
+++ b/XP.Common/Localization/Implementations/ResourceSource.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Resources;
+
+namespace XP.Common.Localization.Implementations
+{
+ ///
+ /// 资源源条目,封装名称与 ResourceManager 的映射
+ /// Resource source entry, encapsulating the mapping between name and ResourceManager
+ ///
+ internal class ResourceSource
+ {
+ ///
+ /// 资源源唯一标识 | Resource source unique identifier
+ ///
+ public string Name { get; }
+
+ ///
+ /// .NET 资源管理器实例 | .NET ResourceManager instance
+ ///
+ public ResourceManager ResourceManager { get; }
+
+ ///
+ /// 构造函数 | Constructor
+ ///
+ /// 资源源名称(如 "XP.Scan")| Resource source name (e.g. "XP.Scan")
+ /// 模块的 ResourceManager 实例 | Module's ResourceManager instance
+ /// 当 name 或 resourceManager 为 null 时抛出 | Thrown when name or resourceManager is null
+ public ResourceSource(string name, ResourceManager resourceManager)
+ {
+ Name = name ?? throw new ArgumentNullException(nameof(name));
+ ResourceManager = resourceManager ?? throw new ArgumentNullException(nameof(resourceManager));
+ }
+ }
+}
diff --git a/XP.Common/Localization/Implementations/ResxLocalizationService.cs b/XP.Common/Localization/Implementations/ResxLocalizationService.cs
new file mode 100644
index 0000000..dd6ae8a
--- /dev/null
+++ b/XP.Common/Localization/Implementations/ResxLocalizationService.cs
@@ -0,0 +1,311 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using System.Resources;
+using System.Threading;
+using Prism.Events;
+using XP.Common.Localization.Enums;
+using XP.Common.Localization.Events;
+using XP.Common.Localization.Interfaces;
+using XP.Common.Logging.Interfaces;
+
+namespace XP.Common.Localization.Implementations
+{
+ ///
+ /// Resx 本地化服务实现 | Resx localization service implementation
+ /// 使用 .NET ResourceManager 加载 Resx 资源文件 | Uses .NET ResourceManager to load Resx resource files
+ /// 支持多资源源 Fallback Chain 机制 | Supports multi-source Fallback Chain mechanism
+ ///
+ public class ResxLocalizationService : ILocalizationService
+ {
+ private readonly List _resourceSources = new List();
+ private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();
+ private readonly IEventAggregator _eventAggregator;
+ private readonly ILocalizationConfig _config;
+ private readonly ILoggerService _logger;
+ private SupportedLanguage _currentLanguage;
+
+ ///
+ /// 获取当前语言 | Get current language
+ ///
+ public SupportedLanguage CurrentLanguage => _currentLanguage;
+
+ ///
+ /// 语言切换事件 | Language changed event
+ ///
+ public event EventHandler? LanguageChanged;
+
+ ///
+ /// 构造函数 | Constructor
+ ///
+ /// 事件聚合器 | Event aggregator
+ /// 本地化配置 | Localization configuration
+ /// 日志服务 | Logger service
+ public ResxLocalizationService(
+ IEventAggregator eventAggregator,
+ ILocalizationConfig config,
+ ILoggerService logger)
+ {
+ _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
+ _config = config ?? throw new ArgumentNullException(nameof(config));
+ _logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule();
+
+ // 初始化默认资源源(XP.Common),注册到 Fallback Chain[0]
+ // Initialize default resource source (XP.Common), registered at Fallback Chain[0]
+ var defaultResourceManager = new ResourceManager(
+ "XP.Common.Resources.Resources",
+ Assembly.GetExecutingAssembly());
+ _resourceSources.Add(new ResourceSource("XP.Common", defaultResourceManager));
+
+ // 加载保存的语言或使用默认语言 | Load saved language or use default language
+ InitializeLanguage();
+ }
+
+ ///
+ /// 获取本地化字符串(使用当前语言)| Get localized string (using current language)
+ ///
+ /// 资源键 | Resource key
+ /// 本地化字符串 | Localized string
+ public string GetString(string key)
+ {
+ return GetString(key, CultureInfo.CurrentUICulture);
+ }
+
+ ///
+ /// 获取本地化字符串(指定语言)| Get localized string (specified language)
+ /// 从 Fallback Chain 末尾向前遍历,第一个返回非 null 值的 ResourceManager 即为结果
+ /// Traverses the Fallback Chain from end to beginning, first ResourceManager returning non-null wins
+ ///
+ /// 资源键 | Resource key
+ /// 文化信息 | Culture info
+ /// 本地化字符串 | Localized string
+ public string GetString(string key, CultureInfo culture)
+ {
+ if (string.IsNullOrEmpty(key))
+ {
+ return string.Empty;
+ }
+
+ _rwLock.EnterReadLock();
+ try
+ {
+ // 从末尾向前遍历(后注册 = 高优先级)
+ // Traverse from end to beginning (last registered = highest priority)
+ for (int i = _resourceSources.Count - 1; i >= 0; i--)
+ {
+ try
+ {
+ var value = _resourceSources[i].ResourceManager.GetString(key, culture);
+ if (value != null)
+ {
+ return value;
+ }
+ }
+ catch (Exception ex)
+ {
+ // 单个 ResourceManager 抛出异常时捕获并继续遍历下一个
+ // Catch exception from individual ResourceManager and continue to next
+ _logger.Error(ex, $"资源源 '{_resourceSources[i].Name}' 查找键 '{key}' 时出错 | Error looking up key '{key}' in resource source '{_resourceSources[i].Name}'");
+ }
+ }
+ }
+ finally
+ {
+ _rwLock.ExitReadLock();
+ }
+
+ // 全部未找到则返回 key 本身并记录警告日志
+ // If all sources fail to find the key, return the key itself and log warning
+ _logger.Warn($"资源键未找到 | Resource key not found: '{key}' for culture '{culture?.Name ?? "null"}'");
+ return key;
+ }
+
+ ///
+ /// 注册模块资源源到 Fallback Chain 末尾(最高优先级)
+ /// Register a module resource source to the end of the Fallback Chain (highest priority)
+ ///
+ /// 资源源名称 | Resource source name
+ /// 模块的 ResourceManager 实例 | Module's ResourceManager instance
+ public void RegisterResourceSource(string name, ResourceManager resourceManager)
+ {
+ if (name == null) throw new ArgumentNullException(nameof(name));
+ if (resourceManager == null) throw new ArgumentNullException(nameof(resourceManager));
+
+ _rwLock.EnterWriteLock();
+ try
+ {
+ // 检查重复名称 | Check for duplicate name
+ if (_resourceSources.Any(s => s.Name == name))
+ {
+ _logger.Warn($"资源源 '{name}' 已存在,注册被拒绝 | Resource source '{name}' already exists, registration rejected");
+ throw new InvalidOperationException($"Resource source '{name}' is already registered.");
+ }
+
+ _resourceSources.Add(new ResourceSource(name, resourceManager));
+ _logger.Info($"资源源 '{name}' 已注册到 Fallback Chain | Resource source '{name}' registered to Fallback Chain");
+ }
+ finally
+ {
+ _rwLock.ExitWriteLock();
+ }
+ }
+
+ ///
+ /// 从 Fallback Chain 中注销指定资源源
+ /// Unregister the specified resource source from the Fallback Chain
+ ///
+ /// 资源源名称 | Resource source name
+ public void UnregisterResourceSource(string name)
+ {
+ if (name == null) throw new ArgumentNullException(nameof(name));
+
+ // 禁止注销默认资源源 | Prevent unregistering default resource source
+ if (name == "XP.Common")
+ {
+ throw new InvalidOperationException("Cannot unregister the default resource source 'XP.Common'.");
+ }
+
+ _rwLock.EnterWriteLock();
+ try
+ {
+ var source = _resourceSources.FirstOrDefault(s => s.Name == name);
+ if (source != null)
+ {
+ _resourceSources.Remove(source);
+ _logger.Info($"资源源 '{name}' 已从 Fallback Chain 注销 | Resource source '{name}' unregistered from Fallback Chain");
+ }
+ else
+ {
+ // 注销不存在的名称时静默忽略,记录警告日志
+ // Silently ignore non-existent name, log warning
+ _logger.Warn($"资源源 '{name}' 不存在,注销操作被忽略 | Resource source '{name}' does not exist, unregister ignored");
+ }
+ }
+ finally
+ {
+ _rwLock.ExitWriteLock();
+ }
+ }
+
+ ///
+ /// 设置当前语言(仅保存配置,重启后生效)| Set current language (save config only, takes effect after restart)
+ ///
+ /// 目标语言 | Target language
+ public void SetLanguage(SupportedLanguage language)
+ {
+ _rwLock.EnterWriteLock();
+ try
+ {
+ // 如果语言相同,无需保存 | If language is the same, no need to save
+ if (_currentLanguage == language)
+ {
+ _logger.Info($"语言已经是 {language},无需切换 | Language is already {language}, no need to switch");
+ return;
+ }
+
+ try
+ {
+ _logger.Info($"保存语言设置,重启后生效 | Saving language setting, takes effect after restart: {_currentLanguage} -> {language}");
+
+ // 仅保存到配置,不更新运行时状态 | Only save to config, do not update runtime state
+ _config.SaveLanguage(language);
+
+ _logger.Info($"语言设置已保存 | Language setting saved: {language}");
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, $"语言设置保存失败 | Failed to save language setting: {language}");
+ throw;
+ }
+ }
+ finally
+ {
+ _rwLock.ExitWriteLock();
+ }
+ }
+
+ ///
+ /// 获取所有支持的语言 | Get all supported languages
+ ///
+ /// 支持的语言列表 | List of supported languages
+ public IEnumerable GetSupportedLanguages()
+ {
+ return Enum.GetValues(typeof(SupportedLanguage))
+ .Cast();
+ }
+
+ ///
+ /// 初始化语言设置 | Initialize language settings
+ ///
+ private void InitializeLanguage()
+ {
+ try
+ {
+ // 尝试获取保存的语言 | Try to get saved language
+ var savedLanguage = _config.GetSavedLanguage();
+ var language = savedLanguage ?? _config.GetSystemDefaultLanguage();
+
+ _currentLanguage = language;
+ var culture = GetCultureInfo(language);
+ CultureInfo.CurrentUICulture = culture;
+ CultureInfo.CurrentCulture = culture;
+
+ _logger.Info($"语言初始化完成 | Language initialized: {language} (Culture: {culture.Name})");
+
+ // 验证资源文件是否可加载 | Verify resource file can be loaded
+ ValidateResourceFile(culture);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "语言初始化失败 | Language initialization failed");
+ throw;
+ }
+ }
+
+ ///
+ /// 获取语言对应的 CultureInfo | Get CultureInfo for language
+ ///
+ /// 支持的语言 | Supported language
+ /// 文化信息 | Culture info
+ private CultureInfo GetCultureInfo(SupportedLanguage language)
+ {
+ return language switch
+ {
+ SupportedLanguage.ZhCN => new CultureInfo("zh-CN"),
+ SupportedLanguage.ZhTW => new CultureInfo("zh-TW"),
+ SupportedLanguage.EnUS => new CultureInfo("en-US"),
+ _ => new CultureInfo("zh-CN") // 默认简体中文 | Default to Simplified Chinese
+ };
+ }
+
+ ///
+ /// 验证资源文件是否可加载 | Validate resource file can be loaded
+ /// 使用默认资源源(XP.Common)进行验证 | Uses default resource source (XP.Common) for validation
+ ///
+ /// 文化信息 | Culture info
+ private void ValidateResourceFile(CultureInfo culture)
+ {
+ try
+ {
+ // 使用第一个资源源(XP.Common)进行验证
+ // Use the first resource source (XP.Common) for validation
+ var testValue = _resourceSources[0].ResourceManager.GetString("App_Title", culture);
+
+ if (testValue != null)
+ {
+ _logger.Info($"资源文件验证成功 | Resource file validated for culture: {culture.Name}");
+ }
+ else
+ {
+ _logger.Warn($"资源文件可能不完整 | Resource file may be incomplete for culture: {culture.Name}");
+ }
+ }
+ catch (Exception)
+ {
+ _logger.Warn($"资源文件验证失败,将使用默认资源 | Resource file validation failed, will use default resources for culture: {culture.Name}");
+ }
+ }
+ }
+}
diff --git a/XP.Common/Localization/Interfaces/ILocalizationConfig.cs b/XP.Common/Localization/Interfaces/ILocalizationConfig.cs
new file mode 100644
index 0000000..c5addc5
--- /dev/null
+++ b/XP.Common/Localization/Interfaces/ILocalizationConfig.cs
@@ -0,0 +1,29 @@
+using XP.Common.Localization.Enums;
+
+namespace XP.Common.Localization.Interfaces
+{
+ ///
+ /// 本地化配置接口 | Localization configuration interface
+ /// 负责语言设置的加载和保存 | Responsible for loading and saving language settings
+ ///
+ public interface ILocalizationConfig
+ {
+ ///
+ /// 获取保存的语言设置 | Get saved language setting
+ ///
+ /// 语言设置,如果未设置则返回 null | Language setting, or null if not set
+ SupportedLanguage? GetSavedLanguage();
+
+ ///
+ /// 保存语言设置 | Save language setting
+ ///
+ /// 要保存的语言 | Language to save
+ void SaveLanguage(SupportedLanguage language);
+
+ ///
+ /// 获取系统默认语言 | Get system default language
+ ///
+ /// 系统默认语言 | System default language
+ SupportedLanguage GetSystemDefaultLanguage();
+ }
+}
diff --git a/XP.Common/Localization/Interfaces/ILocalizationService.cs b/XP.Common/Localization/Interfaces/ILocalizationService.cs
new file mode 100644
index 0000000..675bb62
--- /dev/null
+++ b/XP.Common/Localization/Interfaces/ILocalizationService.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Resources;
+using XP.Common.Localization.Enums;
+using XP.Common.Localization.Events;
+
+namespace XP.Common.Localization.Interfaces
+{
+ ///
+ /// 本地化服务接口 | Localization service interface
+ /// 提供多语言资源访问和语言切换功能 | Provides multilingual resource access and language switching
+ ///
+ public interface ILocalizationService
+ {
+ ///
+ /// 获取当前语言 | Get current language
+ ///
+ SupportedLanguage CurrentLanguage { get; }
+
+ ///
+ /// 语言切换事件 | Language changed event
+ ///
+ event EventHandler LanguageChanged;
+
+ ///
+ /// 获取本地化字符串(使用当前语言)| Get localized string (using current language)
+ ///
+ /// 资源键 | Resource key
+ /// 本地化字符串 | Localized string
+ string GetString(string key);
+
+ ///
+ /// 获取本地化字符串(指定语言)| Get localized string (specified language)
+ ///
+ /// 资源键 | Resource key
+ /// 文化信息 | Culture info
+ /// 本地化字符串 | Localized string
+ string GetString(string key, CultureInfo culture);
+
+ ///
+ /// 设置当前语言 | Set current language
+ ///
+ /// 目标语言 | Target language
+ void SetLanguage(SupportedLanguage language);
+
+ ///
+ /// 获取所有支持的语言 | Get all supported languages
+ ///
+ /// 支持的语言列表 | List of supported languages
+ IEnumerable GetSupportedLanguages();
+
+ ///
+ /// 注册模块资源源到 Fallback Chain 末尾(最高优先级)
+ /// Register a module resource source to the end of the Fallback Chain (highest priority)
+ ///
+ /// 资源源名称(如 "XP.Scan"),用于标识和注销 | Resource source name (e.g. "XP.Scan"), used for identification and unregistration
+ /// 模块的 ResourceManager 实例 | The module's ResourceManager instance
+ void RegisterResourceSource(string name, ResourceManager resourceManager);
+
+ ///
+ /// 从 Fallback Chain 中注销指定资源源
+ /// Unregister the specified resource source from the Fallback Chain
+ ///
+ /// 资源源名称 | Resource source name
+ void UnregisterResourceSource(string name);
+ }
+}
diff --git a/XP.Common/Localization/ViewModels/LanguageOption.cs b/XP.Common/Localization/ViewModels/LanguageOption.cs
new file mode 100644
index 0000000..f73df2a
--- /dev/null
+++ b/XP.Common/Localization/ViewModels/LanguageOption.cs
@@ -0,0 +1,39 @@
+using XP.Common.Localization.Enums;
+
+namespace XP.Common.Localization.ViewModels
+{
+ ///
+ /// 语言选项数据模型 | Language option data model
+ /// 用于在 UI 中显示可选语言 | Used to display available languages in UI
+ ///
+ public class LanguageOption
+ {
+ ///
+ /// 语言枚举值 | Language enum value
+ ///
+ public SupportedLanguage Language { get; }
+
+ ///
+ /// 显示名称 | Display name
+ ///
+ public string DisplayName { get; }
+
+ ///
+ /// 语言标志/图标 | Language flag/icon
+ ///
+ public string Flag { get; }
+
+ ///
+ /// 构造函数 | Constructor
+ ///
+ /// 语言 | Language
+ /// 显示名称 | Display name
+ /// 标志 | Flag
+ public LanguageOption(SupportedLanguage language, string displayName, string flag = "")
+ {
+ Language = language;
+ DisplayName = displayName;
+ Flag = flag;
+ }
+ }
+}
diff --git a/XP.Common/Localization/ViewModels/LanguageSwitcherViewModel.cs b/XP.Common/Localization/ViewModels/LanguageSwitcherViewModel.cs
new file mode 100644
index 0000000..51d5110
--- /dev/null
+++ b/XP.Common/Localization/ViewModels/LanguageSwitcherViewModel.cs
@@ -0,0 +1,209 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Windows;
+using Prism.Commands;
+using Prism.Mvvm;
+using XP.Common.Localization.Enums;
+using XP.Common.Localization.Interfaces;
+using XP.Common.Logging.Interfaces;
+
+
+namespace XP.Common.Localization.ViewModels
+{
+ ///
+ /// 语言切换器 ViewModel | Language switcher ViewModel
+ /// 提供语言选择和切换功能 | Provides language selection and switching functionality
+ ///
+ public class LanguageSwitcherViewModel : BindableBase
+ {
+ private readonly ILocalizationService _localizationService;
+ private readonly ILocalizationConfig _config;
+ private readonly ILoggerService _logger;
+ private LanguageOption? _selectedLanguage;
+
+ ///
+ /// 选中的语言选项 | Selected language option
+ ///
+ public LanguageOption? SelectedLanguage
+ {
+ get => _selectedLanguage;
+ set
+ {
+ if (SetProperty(ref _selectedLanguage, value))
+ {
+ ApplyCommand.RaiseCanExecuteChanged();
+ UpdatePreviewTexts();
+ }
+ }
+ }
+
+ ///
+ /// 可用语言列表 | Available languages list
+ ///
+ public IEnumerable AvailableLanguages { get; }
+
+ ///
+ /// 应用按钮命令 | Apply button command
+ ///
+ public DelegateCommand ApplyCommand { get; }
+
+ private string _previewRestartNotice = string.Empty;
+ ///
+ /// 预览重启提示文本 | Preview restart notice text
+ ///
+ public string PreviewRestartNotice
+ {
+ get => _previewRestartNotice;
+ set => SetProperty(ref _previewRestartNotice, value);
+ }
+
+ private string _previewApplyButtonText = string.Empty;
+ ///
+ /// 预览应用按钮文本 | Preview apply button text
+ ///
+ public string PreviewApplyButtonText
+ {
+ get => _previewApplyButtonText;
+ set => SetProperty(ref _previewApplyButtonText, value);
+ }
+
+ ///
+ /// 构造函数 | Constructor
+ ///
+ /// 本地化服务 | Localization service
+ /// 本地化配置 | Localization configuration
+ /// 日志服务 | Logger service
+ public LanguageSwitcherViewModel(
+ ILocalizationService localizationService,
+ ILocalizationConfig config,
+ ILoggerService logger)
+ {
+ _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService));
+ _config = config ?? throw new ArgumentNullException(nameof(config));
+ _logger = logger.ForModule();
+
+ // 初始化可用语言列表 | Initialize available languages list
+ AvailableLanguages = new[]
+ {
+ new LanguageOption(SupportedLanguage.ZhCN, "简体中文", "🇨🇳"),
+ new LanguageOption(SupportedLanguage.ZhTW, "繁體中文", "🇹🇼"),
+ new LanguageOption(SupportedLanguage.EnUS, "English", "🇺🇸")
+ };
+
+ // 设置当前选中的语言 | Set currently selected language
+ var currentLang = localizationService.CurrentLanguage;
+ foreach (var lang in AvailableLanguages)
+ {
+ if (lang.Language == currentLang)
+ {
+ _selectedLanguage = lang;
+ break;
+ }
+ }
+
+ // 初始化命令 | Initialize commands
+ ApplyCommand = new DelegateCommand(OnApply, CanApply);
+
+ // 初始化预览文本 | Initialize preview texts
+ UpdatePreviewTexts();
+ }
+
+ ///
+ /// 更新预览文本 | Update preview texts
+ ///
+ private void UpdatePreviewTexts()
+ {
+ if (_selectedLanguage == null)
+ return;
+
+ // 获取选中语言的 CultureInfo | Get CultureInfo for selected language
+ var previewCulture = GetCultureInfo(_selectedLanguage.Language);
+
+ // 使用选中的语言获取本地化文本 | Get localized text using selected language
+ PreviewRestartNotice = _localizationService.GetString("Settings_Language_RestartNotice", previewCulture);
+ PreviewApplyButtonText = _localizationService.GetString("Button_Apply", previewCulture);
+ }
+
+ ///
+ /// 判断是否可以应用 | Determine if can apply
+ /// 与配置文件中已保存的语言比较 | Compare with saved language in config
+ ///
+ private bool CanApply()
+ {
+ if (_selectedLanguage == null)
+ return false;
+
+ // 获取配置文件中已保存的语言 | Get saved language from config
+ var savedLanguage = _config.GetSavedLanguage() ?? _localizationService.CurrentLanguage;
+ return _selectedLanguage.Language != savedLanguage;
+ }
+
+ ///
+ /// 应用语言切换 | Apply language change
+ ///
+ private void OnApply()
+ {
+ if (_selectedLanguage == null)
+ return;
+
+ try
+ {
+ var newLanguage = _selectedLanguage.Language;
+ _localizationService.SetLanguage(newLanguage);
+
+ _logger.Info($"Language switched to {newLanguage}");
+
+ // 显示重启提示对话框 | Show restart prompt dialog
+ MessageBox.Show(
+ _localizationService.GetString("Settings_Language_SavedRestartRequired"),
+ _localizationService.GetString("Dialog_Notice"),
+ MessageBoxButton.OK,
+ MessageBoxImage.Information);
+
+ // 更新命令状态 | Update command state
+ ApplyCommand.RaiseCanExecuteChanged();
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, $"Failed to switch language to {_selectedLanguage.Language}");
+
+ // 显示错误对话框 | Show error dialog
+ var errorMsg = string.Format(
+ _localizationService.GetString("Settings_Language_SwitchFailed"),
+ ex.Message);
+ MessageBox.Show(
+ errorMsg,
+ _localizationService.GetString("Dialog_Error"),
+ MessageBoxButton.OK,
+ MessageBoxImage.Error);
+
+ // 恢复到之前的语言 | Restore previous language
+ var currentLang = _localizationService.CurrentLanguage;
+ foreach (var lang in AvailableLanguages)
+ {
+ if (lang.Language == currentLang)
+ {
+ _selectedLanguage = lang;
+ RaisePropertyChanged(nameof(SelectedLanguage));
+ break;
+ }
+ }
+ }
+ }
+
+ ///
+ /// 获取语言对应的 CultureInfo | Get CultureInfo for language
+ ///
+ private CultureInfo GetCultureInfo(SupportedLanguage language)
+ {
+ return language switch
+ {
+ SupportedLanguage.ZhCN => new CultureInfo("zh-CN"),
+ SupportedLanguage.ZhTW => new CultureInfo("zh-TW"),
+ SupportedLanguage.EnUS => new CultureInfo("en-US"),
+ _ => new CultureInfo("zh-CN")
+ };
+ }
+ }
+}
diff --git a/XP.Common/Localization/Views/LanguageSwitcherWindow.xaml b/XP.Common/Localization/Views/LanguageSwitcherWindow.xaml
new file mode 100644
index 0000000..a7bf8ff
--- /dev/null
+++ b/XP.Common/Localization/Views/LanguageSwitcherWindow.xaml
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/XP.Common/Localization/Views/LanguageSwitcherWindow.xaml.cs b/XP.Common/Localization/Views/LanguageSwitcherWindow.xaml.cs
new file mode 100644
index 0000000..7cf5b70
--- /dev/null
+++ b/XP.Common/Localization/Views/LanguageSwitcherWindow.xaml.cs
@@ -0,0 +1,28 @@
+using System.Windows;
+using XP.Common.Localization.ViewModels;
+
+namespace XP.Common.Localization.Views
+{
+ ///
+ /// LanguageSwitcherWindow.xaml 的交互逻辑 | Interaction logic for LanguageSwitcherWindow.xaml
+ ///
+ public partial class LanguageSwitcherWindow : Window
+ {
+ public LanguageSwitcherWindow()
+ {
+ InitializeComponent();
+ }
+
+ ///
+ /// 构造函数(带 ViewModel)| Constructor (with ViewModel)
+ ///
+ /// 语言切换器 ViewModel | Language switcher ViewModel
+ public LanguageSwitcherWindow(LanguageSwitcherViewModel viewModel) : this()
+ {
+ DataContext = viewModel;
+
+ if (Application.Current?.MainWindow != null)
+ Icon = Application.Current.MainWindow.Icon;
+ }
+ }
+}
diff --git a/XP.Common/Logging/Implementations/SerilogLoggerService.cs b/XP.Common/Logging/Implementations/SerilogLoggerService.cs
new file mode 100644
index 0000000..f920bc1
--- /dev/null
+++ b/XP.Common/Logging/Implementations/SerilogLoggerService.cs
@@ -0,0 +1,74 @@
+using System;
+using Serilog;
+using XP.Common.Logging.Interfaces;
+
+namespace XP.Common.Logging.Implementations
+{
+ ///
+ /// Serilog日志服务实现(适配ILoggerService接口)| Serilog logger service implementation (adapts ILoggerService interface)
+ ///
+ public class SerilogLoggerService : ILoggerService
+ {
+ private ILogger _logger;
+
+ ///
+ /// 构造函数:初始化全局日志实例 | Constructor: initialize global logger instance
+ ///
+ public SerilogLoggerService()
+ {
+ _logger = Log.Logger;
+ }
+
+ ///
+ /// 私有构造函数:用于模块标记 | Private constructor: for module tagging
+ ///
+ private SerilogLoggerService(ILogger logger)
+ {
+ _logger = logger ?? Log.Logger;
+ }
+
+ public void Debug(string message, params object[] args)
+ {
+ _logger.Debug(message, args);
+ }
+
+ public void Info(string message, params object[] args)
+ {
+ _logger.Information(message, args);
+ }
+
+ public void Warn(string message, params object[] args)
+ {
+ _logger.Warning(message, args);
+ }
+
+ public void Error(Exception ex, string message, params object[] args)
+ {
+ _logger.Error(ex, message, args);
+ }
+
+ public void Fatal(Exception ex, string message, params object[] args)
+ {
+ _logger.Fatal(ex, message, args);
+ }
+
+ ///
+ /// 手动指定模块名 | Manually specify module name
+ ///
+ public ILoggerService ForModule(string moduleName)
+ {
+ if (string.IsNullOrEmpty(moduleName)) return this;
+ return new SerilogLoggerService(_logger.ForContext("SourceContext", moduleName));
+ }
+
+ ///
+ /// 自动使用类型全名作为模块名 | Automatically use type full name as module name
+ ///
+ /// 类型参数 | Type parameter
+ public ILoggerService ForModule()
+ {
+ var typeName = typeof(T).FullName ?? typeof(T).Name;
+ return new SerilogLoggerService(_logger.ForContext("SourceContext", typeName));
+ }
+ }
+}
\ No newline at end of file
diff --git a/XP.Common/Logging/Interfaces/ILoggerService.cs b/XP.Common/Logging/Interfaces/ILoggerService.cs
new file mode 100644
index 0000000..772088a
--- /dev/null
+++ b/XP.Common/Logging/Interfaces/ILoggerService.cs
@@ -0,0 +1,47 @@
+using System;
+
+namespace XP.Common.Logging.Interfaces
+{
+ ///
+ /// 通用日志服务接口(与具体日志框架解耦)| Generic logger service interface (decoupled from specific logging framework)
+ ///
+ public interface ILoggerService
+ {
+ ///
+ /// 调试日志 | Debug log
+ ///
+ void Debug(string message, params object[] args);
+
+ ///
+ /// 信息日志 | Information log
+ ///
+ void Info(string message, params object[] args);
+
+ ///
+ /// 警告日志 | Warning log
+ ///
+ void Warn(string message, params object[] args);
+
+ ///
+ /// 错误日志(带异常)| Error log (with exception)
+ ///
+ void Error(Exception ex, string message, params object[] args);
+
+ ///
+ /// 致命错误日志(带异常)| Fatal error log (with exception)
+ ///
+ void Fatal(Exception ex, string message, params object[] args);
+
+ ///
+ /// 标记日志所属模块(手动指定模块名)| Mark logger module (manually specify module name)
+ ///
+ /// 模块名称 | Module name
+ ILoggerService ForModule(string moduleName);
+
+ ///
+ /// 标记日志所属模块(自动使用类型全名)| Mark logger module (automatically use type full name)
+ ///
+ /// 类型参数(自动推断命名空间+类名)| Type parameter (automatically infer namespace + class name)
+ ILoggerService ForModule();
+ }
+}
\ No newline at end of file
diff --git a/XP.Common/Logging/SerilogInitializer.cs b/XP.Common/Logging/SerilogInitializer.cs
new file mode 100644
index 0000000..1eb3456
--- /dev/null
+++ b/XP.Common/Logging/SerilogInitializer.cs
@@ -0,0 +1,57 @@
+using System;
+using Serilog;
+using Serilog.Events;
+using XP.Common.Configs;
+using XP.Common.Logging.ViewModels;
+
+namespace XP.Common.Logging
+{
+ ///
+ /// Serilog全局初始化工具(应用启动时调用一次)
+ ///
+ public static class SerilogInitializer
+ {
+ ///
+ /// 初始化Serilog日志
+ ///
+ public static void Initialize(SerilogConfig config)
+ {
+ if (config == null) throw new ArgumentNullException(nameof(config));
+
+ // 确保日志目录存在
+ if (!System.IO.Directory.Exists(config.LogPath))
+ System.IO.Directory.CreateDirectory(config.LogPath);
+
+ // 解析日志级别
+ if (!Enum.TryParse(config.MinimumLevel, true, out var minLevel))
+ minLevel = LogEventLevel.Information;
+
+ // 解析文件分割规则
+ if (!Enum.TryParse(config.RollingInterval, true, out var rollingInterval))
+ rollingInterval = RollingInterval.Day;
+
+ // 构建Serilog配置
+ var loggerConfig = new LoggerConfiguration()
+ .MinimumLevel.Is(minLevel)
+ .Enrich.FromLogContext() // 启用上下文(模块标记)
+ .WriteTo.File(
+ path: System.IO.Path.Combine(config.LogPath, "app_.log"), // 最终文件名:app_20260307.log
+ rollingInterval: rollingInterval,
+ fileSizeLimitBytes: config.FileSizeLimitMB * 1024 * 1024,
+ retainedFileCountLimit: config.RetainedFileCountLimit,
+ outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}"
+ );
+
+ // 调试环境输出到控制台
+ if (config.EnableConsole)
+ loggerConfig.WriteTo.Console(
+ outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}");
+
+ // 注册实时日志查看器 Sink(接收所有级别日志)| Register real-time log viewer sink (receive all levels)
+ loggerConfig.WriteTo.Sink(RealTimeLogSink.Instance, Serilog.Events.LogEventLevel.Verbose);
+
+ // 全局初始化
+ Log.Logger = loggerConfig.CreateLogger();
+ }
+ }
+}
\ No newline at end of file
diff --git a/XP.Common/Logging/ViewModels/RealTimeLogSink.cs b/XP.Common/Logging/ViewModels/RealTimeLogSink.cs
new file mode 100644
index 0000000..eea7891
--- /dev/null
+++ b/XP.Common/Logging/ViewModels/RealTimeLogSink.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using Serilog.Core;
+using Serilog.Events;
+
+namespace XP.Common.Logging.ViewModels
+{
+ ///
+ /// Serilog 自定义 Sink,将日志事件转发到 RealTimeLogViewerViewModel,并维护环形缓冲区
+ /// Custom Serilog Sink that forwards log events and maintains a ring buffer for history
+ ///
+ public class RealTimeLogSink : ILogEventSink
+ {
+ private const int BufferCapacity = 500;
+ private readonly ConcurrentQueue _buffer = new();
+
+ ///
+ /// 全局单例实例 | Global singleton instance
+ ///
+ public static RealTimeLogSink Instance { get; } = new();
+
+ ///
+ /// 日志事件到达时触发 | Fired when a log event arrives
+ ///
+ public event Action? LogEventReceived;
+
+ public void Emit(LogEvent logEvent)
+ {
+ // 入队缓冲区 | Enqueue to buffer
+ _buffer.Enqueue(logEvent);
+ while (_buffer.Count > BufferCapacity)
+ {
+ _buffer.TryDequeue(out _);
+ }
+
+ LogEventReceived?.Invoke(logEvent);
+ }
+
+ ///
+ /// 获取缓冲区中的历史日志快照 | Get a snapshot of buffered history logs
+ ///
+ public List GetBufferedHistory()
+ {
+ return _buffer.ToList();
+ }
+ }
+}
diff --git a/XP.Common/Logging/ViewModels/RealTimeLogViewerViewModel.cs b/XP.Common/Logging/ViewModels/RealTimeLogViewerViewModel.cs
new file mode 100644
index 0000000..5c8c785
--- /dev/null
+++ b/XP.Common/Logging/ViewModels/RealTimeLogViewerViewModel.cs
@@ -0,0 +1,329 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Windows;
+using System.Windows.Media;
+using System.Windows.Threading;
+using Prism.Commands;
+using Prism.Mvvm;
+using Serilog.Events;
+using XP.Common.Localization;
+
+namespace XP.Common.Logging.ViewModels
+{
+ ///
+ /// 实时日志查看器 ViewModel,管理日志条目、过滤、自动滚动等逻辑
+ /// Real-time log viewer ViewModel, manages log entries, filtering, auto-scroll, etc.
+ ///
+ public class RealTimeLogViewerViewModel : BindableBase
+ {
+ private readonly Dispatcher _dispatcher;
+ private readonly ObservableCollection _allEntries = new();
+ private string _filterText = string.Empty;
+ private bool _isAutoScroll = true;
+ private int _maxLines = 2000;
+ private int _totalCount;
+ private int _filteredCount;
+ private bool _showDebug = true;
+ private bool _showInfo = true;
+ private bool _showWarning = true;
+ private bool _showError = true;
+ private bool _showFatal = true;
+
+ public RealTimeLogViewerViewModel()
+ {
+ _dispatcher = Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher;
+
+ FilteredEntries = new ObservableCollection();
+ ClearCommand = new DelegateCommand(ClearAll);
+ }
+
+ ///
+ /// 过滤后的日志条目集合(绑定到 UI)| Filtered log entries (bound to UI)
+ ///
+ public ObservableCollection FilteredEntries { get; }
+
+ ///
+ /// 过滤关键词 | Filter keyword
+ ///
+ public string FilterText
+ {
+ get => _filterText;
+ set
+ {
+ if (SetProperty(ref _filterText, value))
+ {
+ ApplyFilter();
+ }
+ }
+ }
+
+ ///
+ /// 是否自动滚动到底部 | Whether to auto-scroll to bottom
+ ///
+ public bool IsAutoScroll
+ {
+ get => _isAutoScroll;
+ set => SetProperty(ref _isAutoScroll, value);
+ }
+
+ ///
+ /// 最大行数限制 | Maximum line count limit
+ ///
+ public int MaxLines
+ {
+ get => _maxLines;
+ set => SetProperty(ref _maxLines, value);
+ }
+
+ ///
+ /// 总日志条数 | Total log entry count
+ ///
+ public int TotalCount
+ {
+ get => _totalCount;
+ private set => SetProperty(ref _totalCount, value);
+ }
+
+ ///
+ /// 过滤后的日志条数 | Filtered log entry count
+ ///
+ public int FilteredCount
+ {
+ get => _filteredCount;
+ private set => SetProperty(ref _filteredCount, value);
+ }
+
+ ///
+ /// 是否显示 Debug 级别日志 | Whether to show Debug level logs
+ ///
+ public bool ShowDebug
+ {
+ get => _showDebug;
+ set { if (SetProperty(ref _showDebug, value)) ApplyFilter(); }
+ }
+
+ ///
+ /// 是否显示 Info 级别日志 | Whether to show Information level logs
+ ///
+ public bool ShowInfo
+ {
+ get => _showInfo;
+ set { if (SetProperty(ref _showInfo, value)) ApplyFilter(); }
+ }
+
+ ///
+ /// 是否显示 Warning 级别日志 | Whether to show Warning level logs
+ ///
+ public bool ShowWarning
+ {
+ get => _showWarning;
+ set { if (SetProperty(ref _showWarning, value)) ApplyFilter(); }
+ }
+
+ ///
+ /// 是否显示 Error 级别日志 | Whether to show Error level logs
+ ///
+ public bool ShowError
+ {
+ get => _showError;
+ set { if (SetProperty(ref _showError, value)) ApplyFilter(); }
+ }
+
+ ///
+ /// 是否显示 Fatal 级别日志 | Whether to show Fatal level logs
+ ///
+ public bool ShowFatal
+ {
+ get => _showFatal;
+ set { if (SetProperty(ref _showFatal, value)) ApplyFilter(); }
+ }
+
+ ///
+ /// 状态栏文本 | Status bar text
+ ///
+ public string StatusText
+ {
+ get
+ {
+ bool hasLevelFilter = !(_showDebug && _showInfo && _showWarning && _showError && _showFatal);
+ bool hasTextFilter = !string.IsNullOrEmpty(_filterText);
+ if (hasLevelFilter || hasTextFilter)
+ return LocalizationHelper.Get("LogViewer_StatusFiltered", TotalCount, FilteredCount);
+ return LocalizationHelper.Get("LogViewer_StatusTotal", TotalCount);
+ }
+ }
+
+ ///
+ /// 清空日志命令 | Clear log command
+ ///
+ public DelegateCommand ClearCommand { get; }
+
+ ///
+ /// 自动滚动请求事件,View 层订阅此事件执行滚动 | Auto-scroll request event
+ ///
+ public event Action? ScrollToBottomRequested;
+
+ ///
+ /// 添加日志事件(线程安全,可从任意线程调用)| Add log event (thread-safe)
+ ///
+ public void AddLogEvent(LogEvent logEvent)
+ {
+ if (logEvent == null) return;
+
+ var entry = new LogDisplayEntry(logEvent);
+
+ if (_dispatcher.CheckAccess())
+ {
+ AddEntryInternal(entry);
+ }
+ else
+ {
+ _dispatcher.InvokeAsync(() => AddEntryInternal(entry));
+ }
+ }
+
+ ///
+ /// 内部添加日志条目 | Internal add log entry
+ ///
+ private void AddEntryInternal(LogDisplayEntry entry)
+ {
+ _allEntries.Add(entry);
+
+ // 超出最大行数时移除最旧的条目 | Remove oldest entries when exceeding max lines
+ while (_allEntries.Count > _maxLines)
+ {
+ var removed = _allEntries[0];
+ _allEntries.RemoveAt(0);
+ FilteredEntries.Remove(removed);
+ }
+
+ // 判断是否匹配过滤条件 | Check if entry matches filter
+ if (MatchesFilter(entry))
+ {
+ FilteredEntries.Add(entry);
+ }
+
+ TotalCount = _allEntries.Count;
+ FilteredCount = FilteredEntries.Count;
+ RaisePropertyChanged(nameof(StatusText));
+
+ // 请求自动滚动 | Request auto-scroll
+ if (_isAutoScroll)
+ {
+ ScrollToBottomRequested?.Invoke();
+ }
+ }
+
+ ///
+ /// 应用过滤器 | Apply filter
+ ///
+ private void ApplyFilter()
+ {
+ FilteredEntries.Clear();
+
+ foreach (var entry in _allEntries)
+ {
+ if (MatchesFilter(entry))
+ {
+ FilteredEntries.Add(entry);
+ }
+ }
+
+ FilteredCount = FilteredEntries.Count;
+ RaisePropertyChanged(nameof(StatusText));
+ }
+
+ ///
+ /// 判断条目是否匹配过滤条件 | Check if entry matches filter
+ ///
+ private bool MatchesFilter(LogDisplayEntry entry)
+ {
+ // 级别过滤 | Level filter
+ var levelMatch = entry.LevelFull switch
+ {
+ "Debug" => _showDebug,
+ "Information" => _showInfo,
+ "Warning" => _showWarning,
+ "Error" => _showError,
+ "Fatal" => _showFatal,
+ _ => true
+ };
+ if (!levelMatch) return false;
+
+ // 关键词过滤 | Keyword filter
+ if (string.IsNullOrWhiteSpace(_filterText)) return true;
+ return entry.Message.Contains(_filterText, StringComparison.OrdinalIgnoreCase)
+ || entry.Level.Contains(_filterText, StringComparison.OrdinalIgnoreCase)
+ || entry.Source.Contains(_filterText, StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// 清空所有日志 | Clear all logs
+ ///
+ private void ClearAll()
+ {
+ _allEntries.Clear();
+ FilteredEntries.Clear();
+ TotalCount = 0;
+ FilteredCount = 0;
+ RaisePropertyChanged(nameof(StatusText));
+ }
+ }
+
+ ///
+ /// 日志显示条目模型 | Log display entry model
+ ///
+ public class LogDisplayEntry
+ {
+ public LogDisplayEntry(LogEvent logEvent)
+ {
+ Timestamp = logEvent.Timestamp.LocalDateTime;
+ TimestampDisplay = Timestamp.ToString("HH:mm:ss.fff");
+ Level = logEvent.Level.ToString().Substring(0, 3).ToUpper();
+ LevelFull = logEvent.Level.ToString();
+
+ // 渲染消息文本 | Render message text
+ Message = logEvent.RenderMessage();
+
+ // 如果有异常,追加异常信息 | Append exception info if present
+ if (logEvent.Exception != null)
+ {
+ Message += Environment.NewLine + logEvent.Exception.ToString();
+ }
+
+ // 提取 SourceContext | Extract SourceContext
+ if (logEvent.Properties.TryGetValue("SourceContext", out var sourceValue))
+ {
+ Source = sourceValue.ToString().Trim('"');
+ }
+ else
+ {
+ Source = string.Empty;
+ }
+
+ // 根据日志级别设置颜色 | Set color based on log level
+ LevelColor = logEvent.Level switch
+ {
+ LogEventLevel.Fatal => new SolidColorBrush(Color.FromRgb(180, 0, 0)),
+ LogEventLevel.Error => new SolidColorBrush(Colors.Red),
+ LogEventLevel.Warning => new SolidColorBrush(Color.FromRgb(200, 130, 0)),
+ LogEventLevel.Debug => new SolidColorBrush(Colors.Gray),
+ LogEventLevel.Verbose => new SolidColorBrush(Colors.LightGray),
+ _ => new SolidColorBrush(Color.FromRgb(30, 30, 30))
+ };
+
+ // Freeze 画刷,使其可跨线程访问(避免 DependencySource 线程异常)
+ // Freeze the brush so it can be accessed from any thread
+ LevelColor.Freeze();
+ }
+
+ public DateTime Timestamp { get; }
+ public string TimestampDisplay { get; }
+ public string Level { get; }
+ public string LevelFull { get; }
+ public string Message { get; }
+ public string Source { get; }
+ public SolidColorBrush LevelColor { get; }
+ }
+}
diff --git a/XP.Common/Logging/Views/RealTimeLogViewer.xaml b/XP.Common/Logging/Views/RealTimeLogViewer.xaml
new file mode 100644
index 0000000..9b8f8b8
--- /dev/null
+++ b/XP.Common/Logging/Views/RealTimeLogViewer.xaml
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/XP.Common/Logging/Views/RealTimeLogViewer.xaml.cs b/XP.Common/Logging/Views/RealTimeLogViewer.xaml.cs
new file mode 100644
index 0000000..b6a6cd9
--- /dev/null
+++ b/XP.Common/Logging/Views/RealTimeLogViewer.xaml.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Windows;
+using Serilog.Events;
+using XP.Common.Logging.ViewModels;
+
+namespace XP.Common.GeneralForm.Views
+{
+ ///
+ /// 实时日志查看器窗口,订阅 Serilog 事件并实时显示
+ /// Real-time log viewer window, subscribes to Serilog events and displays in real-time
+ ///
+ public partial class RealTimeLogViewer : Window
+ {
+ private readonly RealTimeLogViewerViewModel _viewModel;
+
+ ///
+ /// 构造函数 | Constructor
+ ///
+ /// 最大行数限制,默认 2000 | Max line count, default 2000
+ public RealTimeLogViewer(int maxLines = 2000)
+ {
+ _viewModel = new RealTimeLogViewerViewModel { MaxLines = maxLines };
+ DataContext = _viewModel;
+
+ InitializeComponent();
+
+ // 继承主窗口图标 | Inherit main window icon
+ if (Application.Current?.MainWindow != null)
+ {
+ Icon = Application.Current.MainWindow.Icon;
+ }
+
+ // 订阅自动滚动事件 | Subscribe to auto-scroll event
+ _viewModel.ScrollToBottomRequested += OnScrollToBottomRequested;
+
+ // 加载缓冲区中的历史日志 | Load buffered history logs
+ var history = RealTimeLogSink.Instance.GetBufferedHistory();
+ foreach (var logEvent in history)
+ {
+ _viewModel.AddLogEvent(logEvent);
+ }
+
+ // 订阅 Serilog Sink 事件(在历史加载之后,避免重复)| Subscribe after history load
+ RealTimeLogSink.Instance.LogEventReceived += OnLogEventReceived;
+
+ Closed += OnWindowClosed;
+ }
+
+ ///
+ /// 接收 Serilog 日志事件 | Receive Serilog log event
+ ///
+ private void OnLogEventReceived(LogEvent logEvent)
+ {
+ _viewModel.AddLogEvent(logEvent);
+ }
+
+ ///
+ /// 自动滚动到底部 | Auto-scroll to bottom
+ ///
+ private void OnScrollToBottomRequested()
+ {
+ try
+ {
+ if (LogGridView.Items.Count > 0)
+ {
+ var lastItem = LogGridView.Items[LogGridView.Items.Count - 1];
+ LogGridView.ScrollIntoView(lastItem);
+ }
+ }
+ catch
+ {
+ // 滚动失败时静默处理 | Silently handle scroll failures
+ }
+ }
+
+ ///
+ /// 窗口关闭时取消订阅,防止内存泄漏 | Unsubscribe on close to prevent memory leaks
+ ///
+ private void OnWindowClosed(object? sender, EventArgs e)
+ {
+ _viewModel.ScrollToBottomRequested -= OnScrollToBottomRequested;
+ RealTimeLogSink.Instance.LogEventReceived -= OnLogEventReceived;
+ }
+ }
+}
diff --git a/XP.Common/Module/CommonModule.cs b/XP.Common/Module/CommonModule.cs
new file mode 100644
index 0000000..1d5c5a8
--- /dev/null
+++ b/XP.Common/Module/CommonModule.cs
@@ -0,0 +1,80 @@
+using System;
+using Prism.Ioc;
+using Prism.Modularity;
+using XP.Common.Dump.Configs;
+using XP.Common.Dump.Implementations;
+using XP.Common.Dump.Interfaces;
+using XP.Common.Helpers;
+using XP.Common.Localization.Configs;
+using XP.Common.Localization.Extensions;
+using XP.Common.Localization.Implementations;
+using XP.Common.Localization.Interfaces;
+using XP.Common.Logging.Interfaces;
+using XP.Common.PdfViewer.Implementations;
+using XP.Common.PdfViewer.Interfaces;
+
+namespace XP.Common.Module
+{
+ ///
+ /// 通用模块 | Common module
+ /// 提供通用基础设施服务,包括本地化支持 | Provides common infrastructure services including localization support
+ ///
+ public class CommonModule : IModule
+ {
+ ///
+ /// 模块初始化 | Module initialization
+ /// 在所有类型注册完成后调用 | Called after all types are registered
+ ///
+ public void OnInitialized(IContainerProvider containerProvider)
+ {
+ // 获取日志服务 | Get logger service
+ var logger = containerProvider.Resolve().ForModule("XP.Common.Module.CommonModule");
+
+ try
+ {
+ // 解析本地化服务 | Resolve localization service
+ var localizationService = containerProvider.Resolve();
+
+ // 初始化 LocalizationExtension | Initialize LocalizationExtension
+ LocalizationExtension.Initialize(localizationService);
+
+ logger.Info("本地化系统初始化成功 | Localization system initialized successfully");
+ logger.Info($"当前语言 | Current language: {localizationService.CurrentLanguage}");
+
+ // 启动 Dump 服务 | Start Dump service
+ var dumpService = containerProvider.Resolve();
+ dumpService.Start();
+ logger.Info("Dump 服务初始化成功 | Dump service initialized successfully");
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, "本地化系统初始化失败 | Localization system initialization failed");
+ throw;
+ }
+ }
+
+ ///
+ /// 注册类型到 DI 容器 | Register types to DI container
+ ///
+ public void RegisterTypes(IContainerRegistry containerRegistry)
+ {
+ // 注册本地化配置服务为单例 | Register localization config service as singleton
+ containerRegistry.RegisterSingleton();
+
+ // 注册本地化服务为单例 | Register localization service as singleton
+ containerRegistry.RegisterSingleton();
+
+ // 注册 Dump 配置为单例(通过工厂方法加载)| Register Dump config as singleton (via factory method)
+ containerRegistry.RegisterSingleton(() => ConfigLoader.LoadDumpConfig());
+
+ // 注册 Dump 服务为单例 | Register Dump service as singleton
+ containerRegistry.RegisterSingleton();
+
+ // 注册 PDF 打印服务为单例 | Register PDF print service as singleton
+ containerRegistry.RegisterSingleton();
+
+ // 注册 PDF 查看服务为单例 | Register PDF viewer service as singleton
+ containerRegistry.RegisterSingleton();
+ }
+ }
+}
\ No newline at end of file
diff --git a/XP.Common/PdfViewer/Exceptions/PdfLoadException.cs b/XP.Common/PdfViewer/Exceptions/PdfLoadException.cs
new file mode 100644
index 0000000..38332c6
--- /dev/null
+++ b/XP.Common/PdfViewer/Exceptions/PdfLoadException.cs
@@ -0,0 +1,22 @@
+using System;
+
+namespace XP.Common.PdfViewer.Exceptions
+{
+ ///
+ /// PDF 加载异常 | PDF load exception
+ /// 当 PDF 文件格式无效或加载失败时抛出 | Thrown when PDF format is invalid or loading fails
+ ///
+ public class PdfLoadException : Exception
+ {
+ ///
+ /// 加载失败的文件路径 | File path that failed to load
+ ///
+ public string? FilePath { get; }
+
+ public PdfLoadException(string message, string? filePath = null, Exception? innerException = null)
+ : base(message, innerException)
+ {
+ FilePath = filePath;
+ }
+ }
+}
diff --git a/XP.Common/PdfViewer/Exceptions/PrintException.cs b/XP.Common/PdfViewer/Exceptions/PrintException.cs
new file mode 100644
index 0000000..2eeb827
--- /dev/null
+++ b/XP.Common/PdfViewer/Exceptions/PrintException.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace XP.Common.PdfViewer.Exceptions
+{
+ ///
+ /// 打印异常 | Print exception
+ /// 当打印过程中发生错误时抛出 | Thrown when an error occurs during printing
+ ///
+ public class PrintException : Exception
+ {
+ public PrintException(string message, Exception? innerException = null)
+ : base(message, innerException) { }
+ }
+}
diff --git a/XP.Common/PdfViewer/Exceptions/PrinterNotFoundException.cs b/XP.Common/PdfViewer/Exceptions/PrinterNotFoundException.cs
new file mode 100644
index 0000000..f55e856
--- /dev/null
+++ b/XP.Common/PdfViewer/Exceptions/PrinterNotFoundException.cs
@@ -0,0 +1,22 @@
+using System;
+
+namespace XP.Common.PdfViewer.Exceptions
+{
+ ///
+ /// 打印机未找到异常 | Printer not found exception
+ /// 当指定的打印机名称不存在或不可用时抛出 | Thrown when specified printer is not found or unavailable
+ ///
+ public class PrinterNotFoundException : Exception
+ {
+ ///
+ /// 未找到的打印机名称 | Name of the printer that was not found
+ ///
+ public string PrinterName { get; }
+
+ public PrinterNotFoundException(string printerName)
+ : base($"打印机未找到 | Printer not found: {printerName}")
+ {
+ PrinterName = printerName;
+ }
+ }
+}
diff --git a/XP.Common/PdfViewer/Implementations/PdfPrintService.cs b/XP.Common/PdfViewer/Implementations/PdfPrintService.cs
new file mode 100644
index 0000000..d9a03cb
--- /dev/null
+++ b/XP.Common/PdfViewer/Implementations/PdfPrintService.cs
@@ -0,0 +1,276 @@
+using System;
+using System.Drawing.Printing;
+using System.IO;
+using System.Windows;
+using System.Windows.Controls;
+using Telerik.Windows.Controls;
+using Telerik.Windows.Documents.Fixed.FormatProviders.Pdf;
+using Telerik.Windows.Documents.Fixed.Print;
+using XP.Common.Logging.Interfaces;
+using XP.Common.PdfViewer.Exceptions;
+using XP.Common.PdfViewer.Interfaces;
+
+namespace XP.Common.PdfViewer.Implementations
+{
+ ///
+ /// PDF 打印服务实现 | PDF print service implementation
+ /// 基于 Telerik RadPdfViewer.Print() 实现 | Based on Telerik RadPdfViewer.Print()
+ ///
+ public class PdfPrintService : IPdfPrintService
+ {
+ private readonly ILoggerService _logger;
+
+ public PdfPrintService(ILoggerService logger)
+ {
+ _logger = logger?.ForModule()
+ ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ ///
+ /// 使用指定打印机打印 PDF 文件 | Print PDF file with specified printer
+ ///
+ public void Print(string filePath, string printerName, int? pageFrom = null, int? pageTo = null, int copies = 1)
+ {
+ // 1. 验证文件路径存在 | Validate file path exists
+ ValidateFilePath(filePath);
+
+ // 2. 验证打印机名称有效 | Validate printer name is valid
+ ValidatePrinterName(printerName);
+
+ RadPdfViewer? pdfViewer = null;
+ try
+ {
+ // 3. 创建隐藏的 RadPdfViewer 并加载 PDF | Create hidden RadPdfViewer and load PDF
+ pdfViewer = CreatePdfViewerWithDocument(filePath);
+
+ // 4. 创建 PrintDialog 并配置打印机名称 | Create PrintDialog and configure printer name
+ var printDialog = new PrintDialog();
+ printDialog.PrintQueue = new System.Printing.PrintQueue(
+ new System.Printing.LocalPrintServer(), printerName);
+
+ // 5. 配置页面范围 | Configure page range
+ if (pageFrom.HasValue || pageTo.HasValue)
+ {
+ printDialog.PageRangeSelection = PageRangeSelection.UserPages;
+ printDialog.PageRange = new PageRange(
+ pageFrom ?? 1,
+ pageTo ?? int.MaxValue);
+ }
+
+ // 6. 配置打印份数 | Configure copies
+ if (copies > 1)
+ {
+ printDialog.PrintTicket.CopyCount = copies;
+ }
+
+ // 7. 配置 Telerik PrintSettings | Configure Telerik PrintSettings
+ var printSettings = new Telerik.Windows.Documents.Fixed.Print.PrintSettings
+ {
+ DocumentName = Path.GetFileName(filePath),
+ PageMargins = new Thickness(0),
+ UseDefaultPrinter = false
+ };
+
+ // 8. 调用 RadPdfViewer.Print() 静默打印 | Call RadPdfViewer.Print() for silent printing
+ pdfViewer.Print(printDialog, printSettings);
+
+ // 9. 记录 Info 日志 | Log info
+ var pageRange = FormatPageRange(pageFrom, pageTo);
+ _logger.Info("打印任务已提交 | Print job submitted: {FileName} → {PrinterName}, 页面范围 | Pages: {PageRange}, 份数 | Copies: {Copies}",
+ Path.GetFileName(filePath), printerName, pageRange, copies);
+ }
+ catch (Exception ex) when (ex is not FileNotFoundException
+ and not PrinterNotFoundException
+ and not PdfLoadException)
+ {
+ _logger.Error(ex, "打印失败 | Print failed: {FileName} → {PrinterName}", Path.GetFileName(filePath), printerName);
+ throw new PrintException($"打印失败 | Print failed: {Path.GetFileName(filePath)}", ex);
+ }
+ finally
+ {
+ // 10. 释放资源 | Release resources
+ DisposePdfViewer(pdfViewer);
+ }
+ }
+
+ ///
+ /// 打开打印设置对话框并打印 | Open print settings dialog and print
+ ///
+ public bool PrintWithDialog(string filePath)
+ {
+ // 1. 验证文件路径 | Validate file path
+ ValidateFilePath(filePath);
+
+ RadPdfViewer? pdfViewer = null;
+ try
+ {
+ // 2. 加载 PDF 到隐藏的 RadPdfViewer | Load PDF into hidden RadPdfViewer
+ pdfViewer = CreatePdfViewerWithDocument(filePath);
+
+ // 3. 配置 PrintSettings | Configure PrintSettings
+ var printSettings = new Telerik.Windows.Documents.Fixed.Print.PrintSettings
+ {
+ DocumentName = Path.GetFileName(filePath),
+ PageMargins = new Thickness(0),
+ UseDefaultPrinter = false
+ };
+
+ // 4. 调用 RadPdfViewer.Print() 弹出 PrintDialog | Call RadPdfViewer.Print() to show PrintDialog
+ pdfViewer.Print(printSettings);
+
+ _logger.Info("用户通过对话框打印 | User printed via dialog: {FileName}", Path.GetFileName(filePath));
+ return true;
+ }
+ catch (Exception ex) when (ex is not FileNotFoundException and not PdfLoadException)
+ {
+ _logger.Error(ex, "打印失败 | Print failed: {FileName}", Path.GetFileName(filePath));
+ throw new PrintException($"打印失败 | Print failed: {Path.GetFileName(filePath)}", ex);
+ }
+ finally
+ {
+ // 5. 释放资源 | Release resources
+ DisposePdfViewer(pdfViewer);
+ }
+ }
+
+ ///
+ /// 打开打印预览对话框 | Open print preview dialog
+ ///
+ public void PrintPreview(string filePath)
+ {
+ // 1. 验证文件路径 | Validate file path
+ ValidateFilePath(filePath);
+
+ RadPdfViewer? pdfViewer = null;
+ try
+ {
+ // 2. 加载 PDF 到隐藏的 RadPdfViewer | Load PDF into hidden RadPdfViewer
+ pdfViewer = CreatePdfViewerWithDocument(filePath);
+
+ // 3. 通过 PrintDialog 显示预览 | Show preview via PrintDialog
+ var printDialog = new PrintDialog();
+
+ var printSettings = new Telerik.Windows.Documents.Fixed.Print.PrintSettings
+ {
+ DocumentName = Path.GetFileName(filePath),
+ PageMargins = new Thickness(0),
+ UseDefaultPrinter = false
+ };
+
+ // 显示打印对话框(含预览功能)| Show print dialog (with preview capability)
+ pdfViewer.Print(printDialog, printSettings);
+
+ _logger.Info("打印预览已显示 | Print preview shown: {FileName}", Path.GetFileName(filePath));
+ }
+ catch (Exception ex) when (ex is not FileNotFoundException and not PdfLoadException)
+ {
+ _logger.Error(ex, "打印预览失败 | Print preview failed: {FileName}", Path.GetFileName(filePath));
+ throw new PrintException($"打印预览失败 | Print preview failed: {Path.GetFileName(filePath)}", ex);
+ }
+ finally
+ {
+ // 4. 释放资源 | Release resources
+ DisposePdfViewer(pdfViewer);
+ }
+ }
+
+ #region 私有辅助方法 | Private Helper Methods
+
+ ///
+ /// 验证文件路径是否存在 | Validate file path exists
+ ///
+ private void ValidateFilePath(string filePath)
+ {
+ if (string.IsNullOrWhiteSpace(filePath))
+ {
+ throw new ArgumentNullException(nameof(filePath), "文件路径不能为空 | File path cannot be null or empty");
+ }
+
+ if (!File.Exists(filePath))
+ {
+ _logger.Error(new FileNotFoundException(filePath), "文件不存在 | File not found: {FilePath}", filePath);
+ throw new FileNotFoundException($"文件不存在 | File not found: {filePath}", filePath);
+ }
+ }
+
+ ///
+ /// 验证打印机名称是否有效 | Validate printer name is valid
+ ///
+ private void ValidatePrinterName(string printerName)
+ {
+ if (string.IsNullOrWhiteSpace(printerName))
+ {
+ throw new ArgumentNullException(nameof(printerName), "打印机名称不能为空 | Printer name cannot be null or empty");
+ }
+
+ // 通过 PrinterSettings.InstalledPrinters 查询系统已安装的打印机 | Query installed printers
+ bool found = false;
+ foreach (string installedPrinter in PrinterSettings.InstalledPrinters)
+ {
+ if (string.Equals(installedPrinter, printerName, StringComparison.OrdinalIgnoreCase))
+ {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ {
+ _logger.Error(new PrinterNotFoundException(printerName),
+ "打印机未找到 | Printer not found: {PrinterName}", printerName);
+ throw new PrinterNotFoundException(printerName);
+ }
+ }
+
+ ///
+ /// 创建隐藏的 RadPdfViewer 并加载 PDF 文档 | Create hidden RadPdfViewer and load PDF document
+ ///
+ private RadPdfViewer CreatePdfViewerWithDocument(string filePath)
+ {
+ var pdfViewer = new RadPdfViewer();
+
+ try
+ {
+ var provider = new PdfFormatProvider();
+ using var fileStream = File.OpenRead(filePath);
+ pdfViewer.Document = provider.Import(fileStream);
+ return pdfViewer;
+ }
+ catch (Exception ex) when (ex is not FileNotFoundException)
+ {
+ // 释放已创建的 viewer | Dispose created viewer
+ DisposePdfViewer(pdfViewer);
+ _logger.Error(ex, "PDF 文件加载失败 | PDF file load failed: {FilePath}", filePath);
+ throw new PdfLoadException("PDF 文件加载失败 | PDF file load failed", filePath, ex);
+ }
+ }
+
+ ///
+ /// 释放 RadPdfViewer 资源 | Dispose RadPdfViewer resources
+ ///
+ private static void DisposePdfViewer(RadPdfViewer? pdfViewer)
+ {
+ if (pdfViewer != null)
+ {
+ pdfViewer.Document = null;
+ }
+ }
+
+ ///
+ /// 格式化页面范围字符串 | Format page range string
+ ///
+ private static string FormatPageRange(int? pageFrom, int? pageTo)
+ {
+ if (!pageFrom.HasValue && !pageTo.HasValue)
+ {
+ return "全部 | All";
+ }
+
+ var from = pageFrom?.ToString() ?? "1";
+ var to = pageTo?.ToString() ?? "末页 | Last";
+ return $"{from}-{to}";
+ }
+
+ #endregion
+ }
+}
diff --git a/XP.Common/PdfViewer/Implementations/PdfViewerService.cs b/XP.Common/PdfViewer/Implementations/PdfViewerService.cs
new file mode 100644
index 0000000..61776d3
--- /dev/null
+++ b/XP.Common/PdfViewer/Implementations/PdfViewerService.cs
@@ -0,0 +1,167 @@
+using System;
+using System.IO;
+using XP.Common.Localization;
+using XP.Common.Logging.Interfaces;
+using XP.Common.PdfViewer.Exceptions;
+using XP.Common.PdfViewer.Interfaces;
+using XP.Common.PdfViewer.ViewModels;
+using XP.Common.PdfViewer.Views;
+
+namespace XP.Common.PdfViewer.Implementations
+{
+ ///
+ /// PDF 查看服务实现 | PDF viewer service implementation
+ /// 基于 Telerik RadPdfViewer 实现 | Based on Telerik RadPdfViewer
+ ///
+ public class PdfViewerService : IPdfViewerService
+ {
+ private readonly ILoggerService _logger;
+ private readonly IPdfPrintService _printService;
+ private bool _disposed;
+
+ public PdfViewerService(ILoggerService logger, IPdfPrintService printService)
+ {
+ _logger = logger?.ForModule()
+ ?? throw new ArgumentNullException(nameof(logger));
+ _printService = printService
+ ?? throw new ArgumentNullException(nameof(printService));
+ }
+
+ ///
+ /// 通过文件路径打开 PDF 阅读器窗口 | Open PDF viewer window by file path
+ ///
+ /// PDF 文件路径 | PDF file path
+ /// 文件不存在 | File not found
+ /// PDF 格式无效 | Invalid PDF format
+ public void OpenViewer(string filePath)
+ {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+
+ // 1. 验证文件路径存在 | Validate file path exists
+ if (string.IsNullOrWhiteSpace(filePath))
+ {
+ throw new ArgumentNullException(nameof(filePath),
+ "文件路径不能为空 | File path cannot be null or empty");
+ }
+
+ if (!File.Exists(filePath))
+ {
+ _logger.Error(new FileNotFoundException(filePath),
+ "文件不存在 | File not found: {FilePath}", filePath);
+ throw new FileNotFoundException(
+ $"文件不存在 | File not found: {filePath}", filePath);
+ }
+
+ try
+ {
+ // 2. 创建 PdfViewerWindowViewModel | Create PdfViewerWindowViewModel
+ var viewModel = new PdfViewerWindowViewModel(filePath, _printService, _logger);
+
+ // 3. 创建 PdfViewerWindow 并设置 DataContext | Create PdfViewerWindow and set DataContext
+ var window = new PdfViewerWindow
+ {
+ DataContext = viewModel
+ };
+
+ // 4. 记录 Info 日志 | Log info
+ _logger.Info("打开 PDF 阅读器窗口 | Opening PDF viewer window: {FileName}",
+ Path.GetFileName(filePath));
+
+ // 5. 显示窗口(非模态)| Show window (non-modal)
+ window.Show();
+ }
+ catch (Exception ex) when (ex is not FileNotFoundException
+ and not ArgumentNullException
+ and not PdfLoadException)
+ {
+ _logger.Error(ex, "PDF 文件加载失败 | PDF file load failed: {FilePath}", filePath);
+ throw new PdfLoadException(
+ "PDF 文件加载失败 | PDF file load failed", filePath, ex);
+ }
+ }
+
+ ///
+ /// 通过文件流打开 PDF 阅读器窗口 | Open PDF viewer window by stream
+ ///
+ /// PDF 文件流 | PDF file stream
+ /// 窗口标题(可选)| Window title (optional)
+ /// 流为 null | Stream is null
+ /// PDF 格式无效 | Invalid PDF format
+ public void OpenViewer(Stream stream, string? title = null)
+ {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+
+ // 1. 验证 stream 非 null | Validate stream is not null
+ ArgumentNullException.ThrowIfNull(stream, nameof(stream));
+
+ try
+ {
+ // 2. 创建 ViewModel | Create ViewModel
+ var viewModel = new PdfViewerWindowViewModel(stream, title, _printService, _logger);
+
+ // 3. 创建 PdfViewerWindow 并设置 DataContext | Create PdfViewerWindow and set DataContext
+ var window = new PdfViewerWindow
+ {
+ DataContext = viewModel
+ };
+
+ // 4. 记录 Info 日志 | Log info
+ var displayTitle = title ?? LocalizationHelper.Get("PdfViewer_Title");
+ _logger.Info("打开 PDF 阅读器窗口(流模式)| Opening PDF viewer window (stream mode): {Title}",
+ displayTitle);
+
+ // 5. 显示窗口(非模态)| Show window (non-modal)
+ window.Show();
+ }
+ catch (Exception ex) when (ex is not ArgumentNullException
+ and not PdfLoadException)
+ {
+ _logger.Error(ex, "PDF 文件加载失败(流模式)| PDF file load failed (stream mode)");
+ throw new PdfLoadException(
+ "PDF 文件加载失败 | PDF file load failed", null, ex);
+ }
+ }
+
+ #region IDisposable 模式 | IDisposable Pattern
+
+ ///
+ /// 释放资源 | Dispose resources
+ ///
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// 释放资源的核心方法 | Core dispose method
+ ///
+ /// 是否由 Dispose() 调用 | Whether called by Dispose()
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+ // 释放托管资源 | Dispose managed resources
+ _logger.Info("PdfViewerService 已释放 | PdfViewerService disposed");
+ }
+
+ _disposed = true;
+ }
+
+ ///
+ /// 终结器安全网 | Finalizer safety net
+ /// 确保未显式释放时仍能清理非托管资源 | Ensures unmanaged resources are cleaned up if not explicitly disposed
+ ///
+ ~PdfViewerService()
+ {
+ Dispose(disposing: false);
+ }
+
+ #endregion
+ }
+}
diff --git a/XP.Common/PdfViewer/Interfaces/IPdfPrintService.cs b/XP.Common/PdfViewer/Interfaces/IPdfPrintService.cs
new file mode 100644
index 0000000..6b45544
--- /dev/null
+++ b/XP.Common/PdfViewer/Interfaces/IPdfPrintService.cs
@@ -0,0 +1,40 @@
+using System;
+using System.IO;
+using XP.Common.PdfViewer.Exceptions;
+
+namespace XP.Common.PdfViewer.Interfaces
+{
+ ///
+ /// PDF 打印服务接口 | PDF print service interface
+ /// 提供 PDF 文件打印功能 | Provides PDF file printing functionality
+ ///
+ public interface IPdfPrintService
+ {
+ ///
+ /// 使用指定打印机打印 PDF 文件 | Print PDF file with specified printer
+ ///
+ /// PDF 文件路径 | PDF file path
+ /// 打印机名称 | Printer name
+ /// 起始页码(从 1 开始,null 表示从第一页)| Start page (1-based, null for first page)
+ /// 结束页码(从 1 开始,null 表示到最后一页)| End page (1-based, null for last page)
+ /// 打印份数(默认 1)| Number of copies (default 1)
+ /// 文件不存在 | File not found
+ /// PDF 格式无效 | Invalid PDF format
+ /// 打印机不存在 | Printer not found
+ /// 打印失败 | Print failed
+ void Print(string filePath, string printerName, int? pageFrom = null, int? pageTo = null, int copies = 1);
+
+ ///
+ /// 打开打印设置对话框并打印 | Open print settings dialog and print
+ ///
+ /// PDF 文件路径 | PDF file path
+ /// 用户是否确认打印 | Whether user confirmed printing
+ bool PrintWithDialog(string filePath);
+
+ ///
+ /// 打开打印预览对话框 | Open print preview dialog
+ ///
+ /// PDF 文件路径 | PDF file path
+ void PrintPreview(string filePath);
+ }
+}
diff --git a/XP.Common/PdfViewer/Interfaces/IPdfViewerService.cs b/XP.Common/PdfViewer/Interfaces/IPdfViewerService.cs
new file mode 100644
index 0000000..d352d2e
--- /dev/null
+++ b/XP.Common/PdfViewer/Interfaces/IPdfViewerService.cs
@@ -0,0 +1,30 @@
+using System;
+using System.IO;
+using XP.Common.PdfViewer.Exceptions;
+
+namespace XP.Common.PdfViewer.Interfaces
+{
+ ///
+ /// PDF 查看服务接口 | PDF viewer service interface
+ /// 提供 PDF 文件加载和阅读器窗口管理功能 | Provides PDF file loading and viewer window management
+ ///
+ public interface IPdfViewerService : IDisposable
+ {
+ ///
+ /// 通过文件路径打开 PDF 阅读器窗口 | Open PDF viewer window by file path
+ ///
+ /// PDF 文件路径 | PDF file path
+ /// 文件不存在 | File not found
+ /// PDF 格式无效 | Invalid PDF format
+ void OpenViewer(string filePath);
+
+ ///
+ /// 通过文件流打开 PDF 阅读器窗口 | Open PDF viewer window by stream
+ ///
+ /// PDF 文件流 | PDF file stream
+ /// 窗口标题(可选)| Window title (optional)
+ /// 流为 null | Stream is null
+ /// PDF 格式无效 | Invalid PDF format
+ void OpenViewer(Stream stream, string? title = null);
+ }
+}
diff --git a/XP.Common/PdfViewer/ViewModels/PdfViewerWindowViewModel.cs b/XP.Common/PdfViewer/ViewModels/PdfViewerWindowViewModel.cs
new file mode 100644
index 0000000..24472c8
--- /dev/null
+++ b/XP.Common/PdfViewer/ViewModels/PdfViewerWindowViewModel.cs
@@ -0,0 +1,138 @@
+using System;
+using System.IO;
+using Prism.Commands;
+using Prism.Mvvm;
+using XP.Common.Localization;
+using XP.Common.Logging.Interfaces;
+using XP.Common.PdfViewer.Interfaces;
+
+namespace XP.Common.PdfViewer.ViewModels
+{
+ ///
+ /// PDF 阅读器窗口 ViewModel | PDF viewer window ViewModel
+ /// 轻量级 ViewModel,核心功能由 RadPdfViewer 控件内置处理 | Lightweight ViewModel, core features handled by RadPdfViewer
+ ///
+ public class PdfViewerWindowViewModel : BindableBase
+ {
+ private readonly IPdfPrintService _printService;
+ private readonly ILoggerService _logger;
+
+ ///
+ /// 窗口标题(含文件名)| Window title (with file name)
+ ///
+ public string Title { get; }
+
+ ///
+ /// PDF 文件路径(用于打印)| PDF file path (for printing)
+ ///
+ public string? FilePath { get; }
+
+ ///
+ /// PDF 文件流(用于流加载场景)| PDF file stream (for stream loading scenario)
+ ///
+ public Stream? PdfStream { get; }
+
+ ///
+ /// 打印命令(打开打印设置对话框)| Print command (open print settings dialog)
+ ///
+ public DelegateCommand PrintCommand { get; }
+
+ ///
+ /// 打印预览命令 | Print preview command
+ ///
+ public DelegateCommand PrintPreviewCommand { get; }
+
+ ///
+ /// 通过文件路径创建 ViewModel | Create ViewModel by file path
+ ///
+ /// PDF 文件路径 | PDF file path
+ /// 打印服务 | Print service
+ /// 日志服务 | Logger service
+ public PdfViewerWindowViewModel(
+ string filePath,
+ IPdfPrintService printService,
+ ILoggerService logger)
+ {
+ _printService = printService ?? throw new ArgumentNullException(nameof(printService));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+
+ FilePath = filePath ?? throw new ArgumentNullException(nameof(filePath));
+ PdfStream = null;
+
+ // 使用多语言标题,包含文件名 | Use localized title with file name
+ var fileName = Path.GetFileName(filePath);
+ Title = LocalizationHelper.Get("PdfViewer_TitleWithFile", fileName);
+
+ // 初始化命令 | Initialize commands
+ PrintCommand = new DelegateCommand(ExecutePrint, CanExecutePrint);
+ PrintPreviewCommand = new DelegateCommand(ExecutePrintPreview, CanExecutePrint);
+ }
+
+ ///
+ /// 通过文件流创建 ViewModel | Create ViewModel by stream
+ ///
+ /// PDF 文件流 | PDF file stream
+ /// 窗口标题(可选)| Window title (optional)
+ /// 打印服务 | Print service
+ /// 日志服务 | Logger service
+ public PdfViewerWindowViewModel(
+ Stream stream,
+ string? title,
+ IPdfPrintService printService,
+ ILoggerService logger)
+ {
+ _printService = printService ?? throw new ArgumentNullException(nameof(printService));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+
+ PdfStream = stream ?? throw new ArgumentNullException(nameof(stream));
+ FilePath = null;
+
+ // 使用自定义标题或默认多语言标题 | Use custom title or default localized title
+ Title = !string.IsNullOrWhiteSpace(title)
+ ? title
+ : LocalizationHelper.Get("PdfViewer_Title");
+
+ // 初始化命令(流模式下无文件路径,命令不可用)| Initialize commands (no file path in stream mode, commands disabled)
+ PrintCommand = new DelegateCommand(ExecutePrint, CanExecutePrint);
+ PrintPreviewCommand = new DelegateCommand(ExecutePrintPreview, CanExecutePrint);
+ }
+
+ ///
+ /// 执行打印(打开打印设置对话框)| Execute print (open print settings dialog)
+ ///
+ private void ExecutePrint()
+ {
+ try
+ {
+ _printService.PrintWithDialog(FilePath!);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "打印失败 | Print failed: {FilePath}", FilePath ?? string.Empty);
+ }
+ }
+
+ ///
+ /// 执行打印预览 | Execute print preview
+ ///
+ private void ExecutePrintPreview()
+ {
+ try
+ {
+ _printService.PrintPreview(FilePath!);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "打印预览失败 | Print preview failed: {FilePath}", FilePath ?? string.Empty);
+ }
+ }
+
+ ///
+ /// 判断打印命令是否可用(仅当 FilePath 有效时)| Check if print command is available (only when FilePath is valid)
+ ///
+ private bool CanExecutePrint()
+ {
+ return !string.IsNullOrEmpty(FilePath);
+ }
+ }
+}
diff --git a/XP.Common/PdfViewer/Views/PdfViewerWindow.xaml b/XP.Common/PdfViewer/Views/PdfViewerWindow.xaml
new file mode 100644
index 0000000..ce8cb06
--- /dev/null
+++ b/XP.Common/PdfViewer/Views/PdfViewerWindow.xaml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/XP.Common/PdfViewer/Views/PdfViewerWindow.xaml.cs b/XP.Common/PdfViewer/Views/PdfViewerWindow.xaml.cs
new file mode 100644
index 0000000..dfb0d1a
--- /dev/null
+++ b/XP.Common/PdfViewer/Views/PdfViewerWindow.xaml.cs
@@ -0,0 +1,132 @@
+using System;
+using System.IO;
+using System.Windows;
+using System.Windows.Controls;
+using Telerik.Windows.Documents.Fixed.FormatProviders.Pdf;
+using XP.Common.PdfViewer.Exceptions;
+using XP.Common.PdfViewer.ViewModels;
+
+namespace XP.Common.PdfViewer.Views
+{
+ ///
+ /// PDF 阅读器窗口 code-behind | PDF viewer window code-behind
+ /// 负责在 Loaded 事件中加载 PDF 文档,在 Closed 事件中释放资源
+ /// Responsible for loading PDF document on Loaded event and releasing resources on Closed event
+ ///
+ public partial class PdfViewerWindow : Window
+ {
+ // 防止缩略图选择和页面跳转之间的循环触发 | Prevent circular trigger between thumbnail selection and page navigation
+ private bool _isSyncingSelection;
+
+ public PdfViewerWindow()
+ {
+ InitializeComponent();
+ Loaded += OnLoaded;
+ Closed += OnClosed;
+ }
+
+ ///
+ /// 窗口加载事件:通过 PdfFormatProvider 加载 PDF 文档到 RadPdfViewer
+ /// Window loaded event: load PDF document into RadPdfViewer via PdfFormatProvider
+ ///
+ private void OnLoaded(object sender, RoutedEventArgs e)
+ {
+ var vm = DataContext as PdfViewerWindowViewModel;
+ if (vm == null) return;
+
+ try
+ {
+ var provider = new PdfFormatProvider();
+
+ if (vm.PdfStream != null)
+ {
+ // 从文件流加载 PDF | Load PDF from stream
+ pdfViewer.Document = provider.Import(vm.PdfStream);
+ }
+ else if (!string.IsNullOrEmpty(vm.FilePath))
+ {
+ // 从文件路径加载 PDF | Load PDF from file path
+ using var stream = File.OpenRead(vm.FilePath);
+ pdfViewer.Document = provider.Import(stream);
+ }
+
+ // 文档加载后,监听页面变化以同步缩略图选中状态 | After document loaded, listen for page changes to sync thumbnail selection
+ pdfViewer.CurrentPageChanged += PdfViewer_CurrentPageChanged;
+
+ // 初始选中第一页缩略图 | Initially select first page thumbnail
+ if (thumbnailListBox.Items.Count > 0)
+ {
+ thumbnailListBox.SelectedIndex = 0;
+ }
+ }
+ catch (Exception ex) when (ex is not PdfLoadException)
+ {
+ // 捕获加载异常并包装为 PdfLoadException | Catch loading exceptions and wrap as PdfLoadException
+ throw new PdfLoadException(
+ "PDF 文件加载失败 | PDF file load failed",
+ vm.FilePath,
+ ex);
+ }
+ }
+
+ ///
+ /// 缩略图列表选择变化:跳转到对应页面 | Thumbnail selection changed: navigate to corresponding page
+ /// ListBox.SelectedIndex 从 0 开始,RadPdfViewer.CurrentPageNumber 从 1 开始
+ /// ListBox.SelectedIndex is 0-based, RadPdfViewer.CurrentPageNumber is 1-based
+ ///
+ private void ThumbnailListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (_isSyncingSelection) return;
+ if (thumbnailListBox.SelectedIndex < 0) return;
+
+ _isSyncingSelection = true;
+ try
+ {
+ // SelectedIndex(0-based) + 1 = CurrentPageNumber(1-based)
+ pdfViewer.CurrentPageNumber = thumbnailListBox.SelectedIndex + 1;
+ }
+ finally
+ {
+ _isSyncingSelection = false;
+ }
+ }
+
+ ///
+ /// PDF 阅读器页面变化:同步缩略图选中状态 | PDF viewer page changed: sync thumbnail selection
+ ///
+ private void PdfViewer_CurrentPageChanged(object? sender, EventArgs e)
+ {
+ if (_isSyncingSelection) return;
+
+ _isSyncingSelection = true;
+ try
+ {
+ // CurrentPageNumber(1-based) - 1 = SelectedIndex(0-based)
+ var index = pdfViewer.CurrentPageNumber - 1;
+ if (index >= 0 && index < thumbnailListBox.Items.Count)
+ {
+ thumbnailListBox.SelectedIndex = index;
+ thumbnailListBox.ScrollIntoView(thumbnailListBox.SelectedItem);
+ }
+ }
+ finally
+ {
+ _isSyncingSelection = false;
+ }
+ }
+
+ ///
+ /// 窗口关闭事件:释放 RadPdfViewer 文档资源
+ /// Window closed event: release RadPdfViewer document resources
+ ///
+ private void OnClosed(object? sender, EventArgs e)
+ {
+ // 取消事件订阅 | Unsubscribe events
+ pdfViewer.CurrentPageChanged -= PdfViewer_CurrentPageChanged;
+
+ // RadPdfViewer 内部管理文档资源,设置 Document = null 触发释放
+ // RadPdfViewer internally manages document resources, setting Document = null triggers release
+ pdfViewer.Document = null;
+ }
+ }
+}
diff --git a/XP.Common/Properties/Licenses.licx b/XP.Common/Properties/Licenses.licx
new file mode 100644
index 0000000..216c475
--- /dev/null
+++ b/XP.Common/Properties/Licenses.licx
@@ -0,0 +1 @@
+Telerik.Windows.Controls.RadProgressBar, Telerik.Windows.Controls, Version=2024.1.408.310, Culture=neutral, PublicKeyToken=5803cfa389c90ce7
diff --git a/XP.Common/ReleaseFiles/XP.Common.deps.json b/XP.Common/ReleaseFiles/XP.Common.deps.json
new file mode 100644
index 0000000..115545c
--- /dev/null
+++ b/XP.Common/ReleaseFiles/XP.Common.deps.json
@@ -0,0 +1,1318 @@
+{
+ "runtimeTarget": {
+ "name": ".NETCoreApp,Version=v8.0",
+ "signature": ""
+ },
+ "compilationOptions": {},
+ "targets": {
+ ".NETCoreApp,Version=v8.0": {
+ "XP.Common/1.0.0": {
+ "dependencies": {
+ "Microsoft.Data.Sqlite": "10.0.3",
+ "Prism.Wpf": "9.0.537",
+ "Serilog": "4.3.1",
+ "Serilog.Settings.Configuration": "10.0.0",
+ "Serilog.Sinks.Console": "6.1.1",
+ "Serilog.Sinks.File": "7.0.0",
+ "Telerik.UI.for.Wpf.NetCore.Xaml": "2024.1.408"
+ },
+ "runtime": {
+ "XP.Common.dll": {}
+ },
+ "resources": {
+ "en-US/XP.Common.resources.dll": {
+ "locale": "en-US"
+ },
+ "zh-CN/XP.Common.resources.dll": {
+ "locale": "zh-CN"
+ },
+ "zh-TW/XP.Common.resources.dll": {
+ "locale": "zh-TW"
+ }
+ }
+ },
+ "Microsoft.Bcl.AsyncInterfaces/1.1.1": {
+ "runtime": {
+ "lib/netstandard2.1/Microsoft.Bcl.AsyncInterfaces.dll": {
+ "assemblyVersion": "1.0.0.0",
+ "fileVersion": "4.700.20.21406"
+ }
+ }
+ },
+ "Microsoft.Bcl.HashCode/1.1.0": {
+ "runtime": {
+ "lib/netcoreapp2.1/Microsoft.Bcl.HashCode.dll": {
+ "assemblyVersion": "1.0.0.0",
+ "fileVersion": "4.700.19.56404"
+ }
+ }
+ },
+ "Microsoft.Data.Sqlite/10.0.3": {
+ "dependencies": {
+ "Microsoft.Data.Sqlite.Core": "10.0.3",
+ "SQLitePCLRaw.bundle_e_sqlite3": "2.1.11",
+ "SQLitePCLRaw.core": "2.1.11"
+ }
+ },
+ "Microsoft.Data.Sqlite.Core/10.0.3": {
+ "dependencies": {
+ "SQLitePCLRaw.core": "2.1.11"
+ },
+ "runtime": {
+ "lib/net8.0/Microsoft.Data.Sqlite.dll": {
+ "assemblyVersion": "10.0.3.0",
+ "fileVersion": "10.0.326.7603"
+ }
+ }
+ },
+ "Microsoft.EntityFrameworkCore/3.1.5": {
+ "dependencies": {
+ "Microsoft.Bcl.AsyncInterfaces": "1.1.1",
+ "Microsoft.Bcl.HashCode": "1.1.0",
+ "Microsoft.EntityFrameworkCore.Abstractions": "3.1.5",
+ "Microsoft.Extensions.Caching.Memory": "3.1.5",
+ "Microsoft.Extensions.DependencyInjection": "3.1.5",
+ "Microsoft.Extensions.Logging": "3.1.5"
+ },
+ "runtime": {
+ "lib/netstandard2.0/Microsoft.EntityFrameworkCore.dll": {
+ "assemblyVersion": "3.1.5.0",
+ "fileVersion": "3.100.520.27007"
+ }
+ }
+ },
+ "Microsoft.EntityFrameworkCore.Abstractions/3.1.5": {
+ "runtime": {
+ "lib/netstandard2.0/Microsoft.EntityFrameworkCore.Abstractions.dll": {
+ "assemblyVersion": "3.1.5.0",
+ "fileVersion": "3.100.520.27007"
+ }
+ }
+ },
+ "Microsoft.Extensions.Caching.Abstractions/3.1.5": {
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "10.0.0"
+ },
+ "runtime": {
+ "lib/netcoreapp3.1/Microsoft.Extensions.Caching.Abstractions.dll": {
+ "assemblyVersion": "3.1.5.0",
+ "fileVersion": "3.100.520.27009"
+ }
+ }
+ },
+ "Microsoft.Extensions.Caching.Memory/3.1.5": {
+ "dependencies": {
+ "Microsoft.Extensions.Caching.Abstractions": "3.1.5",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1",
+ "Microsoft.Extensions.Logging.Abstractions": "3.1.5",
+ "Microsoft.Extensions.Options": "3.1.5"
+ },
+ "runtime": {
+ "lib/netcoreapp3.1/Microsoft.Extensions.Caching.Memory.dll": {
+ "assemblyVersion": "3.1.5.0",
+ "fileVersion": "3.100.520.27009"
+ }
+ }
+ },
+ "Microsoft.Extensions.Configuration/10.0.0": {
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.0",
+ "Microsoft.Extensions.Primitives": "10.0.0"
+ },
+ "runtime": {
+ "lib/net8.0/Microsoft.Extensions.Configuration.dll": {
+ "assemblyVersion": "10.0.0.0",
+ "fileVersion": "10.0.25.52411"
+ }
+ }
+ },
+ "Microsoft.Extensions.Configuration.Abstractions/10.0.0": {
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "10.0.0"
+ },
+ "runtime": {
+ "lib/net8.0/Microsoft.Extensions.Configuration.Abstractions.dll": {
+ "assemblyVersion": "10.0.0.0",
+ "fileVersion": "10.0.25.52411"
+ }
+ }
+ },
+ "Microsoft.Extensions.Configuration.Binder/10.0.0": {
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "10.0.0",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.0"
+ },
+ "runtime": {
+ "lib/net8.0/Microsoft.Extensions.Configuration.Binder.dll": {
+ "assemblyVersion": "10.0.0.0",
+ "fileVersion": "10.0.25.52411"
+ }
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection/3.1.5": {
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1"
+ },
+ "runtime": {
+ "lib/netcoreapp3.1/Microsoft.Extensions.DependencyInjection.dll": {
+ "assemblyVersion": "3.1.5.0",
+ "fileVersion": "3.100.520.27009"
+ }
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection.Abstractions/8.0.1": {
+ "runtime": {
+ "lib/net8.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll": {
+ "assemblyVersion": "8.0.0.0",
+ "fileVersion": "8.0.324.11423"
+ }
+ }
+ },
+ "Microsoft.Extensions.DependencyModel/10.0.0": {
+ "dependencies": {
+ "System.Text.Encodings.Web": "10.0.0",
+ "System.Text.Json": "10.0.0"
+ },
+ "runtime": {
+ "lib/net8.0/Microsoft.Extensions.DependencyModel.dll": {
+ "assemblyVersion": "10.0.0.0",
+ "fileVersion": "10.0.25.52411"
+ }
+ }
+ },
+ "Microsoft.Extensions.Logging/3.1.5": {
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Binder": "10.0.0",
+ "Microsoft.Extensions.DependencyInjection": "3.1.5",
+ "Microsoft.Extensions.Logging.Abstractions": "3.1.5",
+ "Microsoft.Extensions.Options": "3.1.5"
+ },
+ "runtime": {
+ "lib/netcoreapp3.1/Microsoft.Extensions.Logging.dll": {
+ "assemblyVersion": "3.1.5.0",
+ "fileVersion": "3.100.520.27009"
+ }
+ }
+ },
+ "Microsoft.Extensions.Logging.Abstractions/3.1.5": {
+ "runtime": {
+ "lib/netstandard2.0/Microsoft.Extensions.Logging.Abstractions.dll": {
+ "assemblyVersion": "3.1.5.0",
+ "fileVersion": "3.100.520.27009"
+ }
+ }
+ },
+ "Microsoft.Extensions.Options/3.1.5": {
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1",
+ "Microsoft.Extensions.Primitives": "10.0.0"
+ },
+ "runtime": {
+ "lib/netcoreapp3.1/Microsoft.Extensions.Options.dll": {
+ "assemblyVersion": "3.1.5.0",
+ "fileVersion": "3.100.520.27009"
+ }
+ }
+ },
+ "Microsoft.Extensions.Primitives/10.0.0": {
+ "runtime": {
+ "lib/net8.0/Microsoft.Extensions.Primitives.dll": {
+ "assemblyVersion": "10.0.0.0",
+ "fileVersion": "10.0.25.52411"
+ }
+ }
+ },
+ "Microsoft.OData.Client/7.8.3": {
+ "dependencies": {
+ "Microsoft.OData.Core": "7.8.3"
+ },
+ "runtime": {
+ "lib/netstandard2.0/Microsoft.OData.Client.dll": {
+ "assemblyVersion": "7.8.3.0",
+ "fileVersion": "7.8.3.20303"
+ }
+ }
+ },
+ "Microsoft.OData.Core/7.8.3": {
+ "dependencies": {
+ "Microsoft.OData.Edm": "7.8.3",
+ "Microsoft.Spatial": "7.8.3"
+ },
+ "runtime": {
+ "lib/netstandard2.0/Microsoft.OData.Core.dll": {
+ "assemblyVersion": "7.8.3.0",
+ "fileVersion": "7.8.3.20303"
+ }
+ }
+ },
+ "Microsoft.OData.Edm/7.8.3": {
+ "dependencies": {
+ "System.Text.Json": "10.0.0"
+ },
+ "runtime": {
+ "lib/netstandard2.0/Microsoft.OData.Edm.dll": {
+ "assemblyVersion": "7.8.3.0",
+ "fileVersion": "7.8.3.20303"
+ }
+ }
+ },
+ "Microsoft.Spatial/7.8.3": {
+ "runtime": {
+ "lib/netstandard2.0/Microsoft.Spatial.dll": {
+ "assemblyVersion": "7.8.3.0",
+ "fileVersion": "7.8.3.20303"
+ }
+ }
+ },
+ "Microsoft.Xaml.Behaviors.Wpf/1.1.122": {
+ "runtime": {
+ "lib/net6.0-windows7.0/Microsoft.Xaml.Behaviors.dll": {
+ "assemblyVersion": "1.1.0.0",
+ "fileVersion": "1.1.122.28819"
+ }
+ }
+ },
+ "Prism.Container.Abstractions/9.0.106": {
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1"
+ },
+ "runtime": {
+ "lib/net8.0/Prism.Container.Abstractions.dll": {
+ "assemblyVersion": "9.0.106.9543",
+ "fileVersion": "9.0.106.9543"
+ }
+ }
+ },
+ "Prism.Core/9.0.537": {
+ "dependencies": {
+ "Prism.Container.Abstractions": "9.0.106",
+ "Prism.Events": "9.0.537"
+ },
+ "runtime": {
+ "lib/net6.0/Prism.dll": {
+ "assemblyVersion": "9.0.537.60525",
+ "fileVersion": "9.0.537.60525"
+ }
+ }
+ },
+ "Prism.Events/9.0.537": {
+ "runtime": {
+ "lib/net6.0/Prism.Events.dll": {
+ "assemblyVersion": "9.0.537.60525",
+ "fileVersion": "9.0.537.60525"
+ }
+ }
+ },
+ "Prism.Wpf/9.0.537": {
+ "dependencies": {
+ "Microsoft.Xaml.Behaviors.Wpf": "1.1.122",
+ "Prism.Core": "9.0.537"
+ },
+ "runtime": {
+ "lib/net6.0-windows7.0/Prism.Wpf.dll": {
+ "assemblyVersion": "9.0.537.60525",
+ "fileVersion": "9.0.537.60525"
+ }
+ }
+ },
+ "Serilog/4.3.1": {
+ "runtime": {
+ "lib/net8.0/Serilog.dll": {
+ "assemblyVersion": "4.3.0.0",
+ "fileVersion": "4.3.0.0"
+ }
+ }
+ },
+ "Serilog.Settings.Configuration/10.0.0": {
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Binder": "10.0.0",
+ "Microsoft.Extensions.DependencyModel": "10.0.0",
+ "Serilog": "4.3.1"
+ },
+ "runtime": {
+ "lib/net8.0/Serilog.Settings.Configuration.dll": {
+ "assemblyVersion": "10.0.0.0",
+ "fileVersion": "10.0.0.0"
+ }
+ }
+ },
+ "Serilog.Sinks.Console/6.1.1": {
+ "dependencies": {
+ "Serilog": "4.3.1"
+ },
+ "runtime": {
+ "lib/net8.0/Serilog.Sinks.Console.dll": {
+ "assemblyVersion": "6.1.1.0",
+ "fileVersion": "6.1.1.0"
+ }
+ }
+ },
+ "Serilog.Sinks.File/7.0.0": {
+ "dependencies": {
+ "Serilog": "4.3.1"
+ },
+ "runtime": {
+ "lib/net8.0/Serilog.Sinks.File.dll": {
+ "assemblyVersion": "7.0.0.0",
+ "fileVersion": "7.0.0.0"
+ }
+ }
+ },
+ "SharpDX/4.2.0": {
+ "runtime": {
+ "lib/netstandard1.1/SharpDX.dll": {
+ "assemblyVersion": "4.2.0.0",
+ "fileVersion": "4.2.0.0"
+ }
+ }
+ },
+ "SharpDX.D3DCompiler/4.2.0": {
+ "dependencies": {
+ "SharpDX": "4.2.0"
+ },
+ "runtime": {
+ "lib/netstandard1.1/SharpDX.D3DCompiler.dll": {
+ "assemblyVersion": "4.2.0.0",
+ "fileVersion": "4.2.0.0"
+ }
+ }
+ },
+ "SharpDX.Direct2D1/4.2.0": {
+ "dependencies": {
+ "SharpDX": "4.2.0",
+ "SharpDX.DXGI": "4.2.0"
+ },
+ "runtime": {
+ "lib/netstandard1.1/SharpDX.Direct2D1.dll": {
+ "assemblyVersion": "4.2.0.0",
+ "fileVersion": "4.2.0.0"
+ }
+ }
+ },
+ "SharpDX.Direct3D10/4.2.0": {
+ "dependencies": {
+ "SharpDX": "4.2.0",
+ "SharpDX.D3DCompiler": "4.2.0",
+ "SharpDX.DXGI": "4.2.0"
+ },
+ "runtime": {
+ "lib/netstandard1.1/SharpDX.Direct3D10.dll": {
+ "assemblyVersion": "4.2.0.0",
+ "fileVersion": "4.2.0.0"
+ }
+ }
+ },
+ "SharpDX.Direct3D9/4.2.0": {
+ "dependencies": {
+ "SharpDX": "4.2.0"
+ },
+ "runtime": {
+ "lib/netstandard1.3/SharpDX.Direct3D9.dll": {
+ "assemblyVersion": "4.2.0.0",
+ "fileVersion": "4.2.0.0"
+ }
+ }
+ },
+ "SharpDX.DXGI/4.2.0": {
+ "dependencies": {
+ "SharpDX": "4.2.0"
+ },
+ "runtime": {
+ "lib/netstandard1.1/SharpDX.DXGI.dll": {
+ "assemblyVersion": "4.2.0.0",
+ "fileVersion": "4.2.0.0"
+ }
+ }
+ },
+ "SharpDX.Mathematics/4.2.0": {
+ "dependencies": {
+ "SharpDX": "4.2.0"
+ },
+ "runtime": {
+ "lib/netstandard1.1/SharpDX.Mathematics.dll": {
+ "assemblyVersion": "4.2.0.0",
+ "fileVersion": "4.2.0.0"
+ }
+ }
+ },
+ "SQLitePCLRaw.bundle_e_sqlite3/2.1.11": {
+ "dependencies": {
+ "SQLitePCLRaw.lib.e_sqlite3": "2.1.11",
+ "SQLitePCLRaw.provider.e_sqlite3": "2.1.11"
+ },
+ "runtime": {
+ "lib/netstandard2.0/SQLitePCLRaw.batteries_v2.dll": {
+ "assemblyVersion": "2.1.11.2622",
+ "fileVersion": "2.1.11.2622"
+ }
+ }
+ },
+ "SQLitePCLRaw.core/2.1.11": {
+ "runtime": {
+ "lib/netstandard2.0/SQLitePCLRaw.core.dll": {
+ "assemblyVersion": "2.1.11.2622",
+ "fileVersion": "2.1.11.2622"
+ }
+ }
+ },
+ "SQLitePCLRaw.lib.e_sqlite3/2.1.11": {
+ "runtimeTargets": {
+ "runtimes/browser-wasm/nativeassets/net8.0/e_sqlite3.a": {
+ "rid": "browser-wasm",
+ "assetType": "native",
+ "fileVersion": "0.0.0.0"
+ },
+ "runtimes/linux-arm/native/libe_sqlite3.so": {
+ "rid": "linux-arm",
+ "assetType": "native",
+ "fileVersion": "0.0.0.0"
+ },
+ "runtimes/linux-arm64/native/libe_sqlite3.so": {
+ "rid": "linux-arm64",
+ "assetType": "native",
+ "fileVersion": "0.0.0.0"
+ },
+ "runtimes/linux-armel/native/libe_sqlite3.so": {
+ "rid": "linux-armel",
+ "assetType": "native",
+ "fileVersion": "0.0.0.0"
+ },
+ "runtimes/linux-mips64/native/libe_sqlite3.so": {
+ "rid": "linux-mips64",
+ "assetType": "native",
+ "fileVersion": "0.0.0.0"
+ },
+ "runtimes/linux-musl-arm/native/libe_sqlite3.so": {
+ "rid": "linux-musl-arm",
+ "assetType": "native",
+ "fileVersion": "0.0.0.0"
+ },
+ "runtimes/linux-musl-arm64/native/libe_sqlite3.so": {
+ "rid": "linux-musl-arm64",
+ "assetType": "native",
+ "fileVersion": "0.0.0.0"
+ },
+ "runtimes/linux-musl-riscv64/native/libe_sqlite3.so": {
+ "rid": "linux-musl-riscv64",
+ "assetType": "native",
+ "fileVersion": "0.0.0.0"
+ },
+ "runtimes/linux-musl-s390x/native/libe_sqlite3.so": {
+ "rid": "linux-musl-s390x",
+ "assetType": "native",
+ "fileVersion": "0.0.0.0"
+ },
+ "runtimes/linux-musl-x64/native/libe_sqlite3.so": {
+ "rid": "linux-musl-x64",
+ "assetType": "native",
+ "fileVersion": "0.0.0.0"
+ },
+ "runtimes/linux-ppc64le/native/libe_sqlite3.so": {
+ "rid": "linux-ppc64le",
+ "assetType": "native",
+ "fileVersion": "0.0.0.0"
+ },
+ "runtimes/linux-riscv64/native/libe_sqlite3.so": {
+ "rid": "linux-riscv64",
+ "assetType": "native",
+ "fileVersion": "0.0.0.0"
+ },
+ "runtimes/linux-s390x/native/libe_sqlite3.so": {
+ "rid": "linux-s390x",
+ "assetType": "native",
+ "fileVersion": "0.0.0.0"
+ },
+ "runtimes/linux-x64/native/libe_sqlite3.so": {
+ "rid": "linux-x64",
+ "assetType": "native",
+ "fileVersion": "0.0.0.0"
+ },
+ "runtimes/linux-x86/native/libe_sqlite3.so": {
+ "rid": "linux-x86",
+ "assetType": "native",
+ "fileVersion": "0.0.0.0"
+ },
+ "runtimes/maccatalyst-arm64/native/libe_sqlite3.dylib": {
+ "rid": "maccatalyst-arm64",
+ "assetType": "native",
+ "fileVersion": "0.0.0.0"
+ },
+ "runtimes/maccatalyst-x64/native/libe_sqlite3.dylib": {
+ "rid": "maccatalyst-x64",
+ "assetType": "native",
+ "fileVersion": "0.0.0.0"
+ },
+ "runtimes/osx-arm64/native/libe_sqlite3.dylib": {
+ "rid": "osx-arm64",
+ "assetType": "native",
+ "fileVersion": "0.0.0.0"
+ },
+ "runtimes/osx-x64/native/libe_sqlite3.dylib": {
+ "rid": "osx-x64",
+ "assetType": "native",
+ "fileVersion": "0.0.0.0"
+ },
+ "runtimes/win-arm/native/e_sqlite3.dll": {
+ "rid": "win-arm",
+ "assetType": "native",
+ "fileVersion": "0.0.0.0"
+ },
+ "runtimes/win-arm64/native/e_sqlite3.dll": {
+ "rid": "win-arm64",
+ "assetType": "native",
+ "fileVersion": "0.0.0.0"
+ },
+ "runtimes/win-x64/native/e_sqlite3.dll": {
+ "rid": "win-x64",
+ "assetType": "native",
+ "fileVersion": "0.0.0.0"
+ },
+ "runtimes/win-x86/native/e_sqlite3.dll": {
+ "rid": "win-x86",
+ "assetType": "native",
+ "fileVersion": "0.0.0.0"
+ }
+ }
+ },
+ "SQLitePCLRaw.provider.e_sqlite3/2.1.11": {
+ "dependencies": {
+ "SQLitePCLRaw.core": "2.1.11"
+ },
+ "runtime": {
+ "lib/net6.0-windows7.0/SQLitePCLRaw.provider.e_sqlite3.dll": {
+ "assemblyVersion": "2.1.11.2622",
+ "fileVersion": "2.1.11.2622"
+ }
+ }
+ },
+ "System.Data.OleDb/6.0.0": {
+ "runtime": {
+ "lib/net6.0/System.Data.OleDb.dll": {
+ "assemblyVersion": "6.0.0.0",
+ "fileVersion": "6.0.21.52210"
+ }
+ },
+ "runtimeTargets": {
+ "runtimes/win/lib/net6.0/System.Data.OleDb.dll": {
+ "rid": "win",
+ "assetType": "runtime",
+ "assemblyVersion": "6.0.0.0",
+ "fileVersion": "6.0.21.52210"
+ }
+ }
+ },
+ "System.IO.Pipelines/10.0.0": {
+ "runtime": {
+ "lib/net8.0/System.IO.Pipelines.dll": {
+ "assemblyVersion": "10.0.0.0",
+ "fileVersion": "10.0.25.52411"
+ }
+ }
+ },
+ "System.Private.ServiceModel/4.7.0": {
+ "runtime": {
+ "lib/netstandard2.0/System.Private.ServiceModel.dll": {
+ "assemblyVersion": "4.7.0.0",
+ "fileVersion": "4.700.19.56502"
+ }
+ }
+ },
+ "System.ServiceModel.Http/4.7.0": {
+ "dependencies": {
+ "System.Private.ServiceModel": "4.7.0",
+ "System.ServiceModel.Primitives": "4.7.0"
+ },
+ "runtime": {
+ "lib/netstandard2.0/System.ServiceModel.Http.dll": {
+ "assemblyVersion": "4.7.0.0",
+ "fileVersion": "4.700.19.56502"
+ }
+ }
+ },
+ "System.ServiceModel.Primitives/4.7.0": {
+ "dependencies": {
+ "System.Private.ServiceModel": "4.7.0"
+ },
+ "runtime": {
+ "lib/netcoreapp2.1/System.ServiceModel.Primitives.dll": {
+ "assemblyVersion": "4.7.0.0",
+ "fileVersion": "4.700.19.56502"
+ },
+ "lib/netcoreapp2.1/System.ServiceModel.dll": {
+ "assemblyVersion": "4.0.0.0",
+ "fileVersion": "4.700.19.56502"
+ }
+ }
+ },
+ "System.Text.Encodings.Web/10.0.0": {
+ "runtime": {
+ "lib/net8.0/System.Text.Encodings.Web.dll": {
+ "assemblyVersion": "10.0.0.0",
+ "fileVersion": "10.0.25.52411"
+ }
+ },
+ "runtimeTargets": {
+ "runtimes/browser/lib/net8.0/System.Text.Encodings.Web.dll": {
+ "rid": "browser",
+ "assetType": "runtime",
+ "assemblyVersion": "10.0.0.0",
+ "fileVersion": "10.0.25.52411"
+ }
+ }
+ },
+ "System.Text.Json/10.0.0": {
+ "dependencies": {
+ "System.IO.Pipelines": "10.0.0",
+ "System.Text.Encodings.Web": "10.0.0"
+ },
+ "runtime": {
+ "lib/net8.0/System.Text.Json.dll": {
+ "assemblyVersion": "10.0.0.0",
+ "fileVersion": "10.0.25.52411"
+ }
+ }
+ },
+ "Telerik.UI.for.Wpf.NetCore.Xaml/2024.1.408": {
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore": "3.1.5",
+ "Microsoft.OData.Client": "7.8.3",
+ "SharpDX": "4.2.0",
+ "SharpDX.DXGI": "4.2.0",
+ "SharpDX.Direct2D1": "4.2.0",
+ "SharpDX.Direct3D10": "4.2.0",
+ "SharpDX.Direct3D9": "4.2.0",
+ "SharpDX.Mathematics": "4.2.0",
+ "System.Data.OleDb": "6.0.0",
+ "System.ServiceModel.Http": "4.7.0"
+ },
+ "runtime": {
+ "lib/netcoreapp3.1/MediaFoundation.dll": {
+ "assemblyVersion": "3.1.0.32450",
+ "fileVersion": "3.1.0.0"
+ },
+ "lib/netcoreapp3.1/Telerik.Documents.SpreadsheetStreaming.dll": {
+ "assemblyVersion": "2024.1.305.310",
+ "fileVersion": "2024.1.305.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Pivot.Core.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Pivot.DataProviders.Adomd.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Pivot.DataProviders.Queryable.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Pivot.DataProviders.Xmla.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Cloud.Controls.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Cloud.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.Chart.Direct2D.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.Chart.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.Charting.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.ConversationalUI.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.Data.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.DataServices.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.DataVisualization.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.Diagrams.Extensions.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.Diagrams.Ribbon.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.Diagrams.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.Docking.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.EntityFrameworkCore.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.Expressions.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.FileDialogs.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.FixedDocumentViewers.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.FixedDocumentViewersUI.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.GanttView.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.GridView.Export.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.GridView.SpreadsheetStreamingExport.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.GridView.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.ImageEditor.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.Input.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.Media.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.Navigation.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.Pivot.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.PivotFieldList.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.RibbonView.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.RichTextBox.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.ScheduleView.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.Spreadsheet.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.SpreadsheetUI.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.SyntaxEditor.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.VirtualGrid.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Controls.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Data.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Diagrams.Core.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Documents.CMapUtils.dll": {
+ "assemblyVersion": "2024.1.305.310",
+ "fileVersion": "2024.1.305.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Documents.Core.dll": {
+ "assemblyVersion": "2024.1.305.310",
+ "fileVersion": "2024.1.305.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Documents.DrawingML.dll": {
+ "assemblyVersion": "2024.1.305.310",
+ "fileVersion": "2024.1.305.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Documents.Fixed.dll": {
+ "assemblyVersion": "2024.1.305.310",
+ "fileVersion": "2024.1.305.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Documents.Flow.FormatProviders.Doc.dll": {
+ "assemblyVersion": "2024.1.305.310",
+ "fileVersion": "2024.1.305.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Documents.Flow.FormatProviders.Pdf.dll": {
+ "assemblyVersion": "2024.1.305.310",
+ "fileVersion": "2024.1.305.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Documents.Flow.dll": {
+ "assemblyVersion": "2024.1.305.310",
+ "fileVersion": "2024.1.305.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Documents.Proofing.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Documents.Spreadsheet.FormatProviders.OpenXml.dll": {
+ "assemblyVersion": "2024.1.305.310",
+ "fileVersion": "2024.1.305.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Documents.Spreadsheet.FormatProviders.Pdf.dll": {
+ "assemblyVersion": "2024.1.305.310",
+ "fileVersion": "2024.1.305.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Documents.Spreadsheet.FormatProviders.Xls.dll": {
+ "assemblyVersion": "2024.1.305.310",
+ "fileVersion": "2024.1.305.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Documents.Spreadsheet.dll": {
+ "assemblyVersion": "2024.1.305.310",
+ "fileVersion": "2024.1.305.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.MediaFoundation.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.PersistenceFramework.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Scheduling.Core.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.SyntaxEditor.Core.dll": {
+ "assemblyVersion": "2024.1.408.310",
+ "fileVersion": "2024.1.408.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Zip.Extensions.dll": {
+ "assemblyVersion": "2024.1.305.310",
+ "fileVersion": "2024.1.305.310"
+ },
+ "lib/netcoreapp3.1/Telerik.Windows.Zip.dll": {
+ "assemblyVersion": "2024.1.305.310",
+ "fileVersion": "2024.1.305.310"
+ }
+ },
+ "resources": {
+ "lib/netcoreapp3.1/de/Telerik.Windows.Controls.resources.dll": {
+ "locale": "de"
+ },
+ "lib/netcoreapp3.1/es/Telerik.Windows.Controls.resources.dll": {
+ "locale": "es"
+ },
+ "lib/netcoreapp3.1/fr/Telerik.Windows.Controls.resources.dll": {
+ "locale": "fr"
+ },
+ "lib/netcoreapp3.1/it/Telerik.Windows.Controls.resources.dll": {
+ "locale": "it"
+ },
+ "lib/netcoreapp3.1/nl/Telerik.Windows.Controls.resources.dll": {
+ "locale": "nl"
+ },
+ "lib/netcoreapp3.1/tr/Telerik.Windows.Controls.resources.dll": {
+ "locale": "tr"
+ }
+ }
+ }
+ }
+ },
+ "libraries": {
+ "XP.Common/1.0.0": {
+ "type": "project",
+ "serviceable": false,
+ "sha512": ""
+ },
+ "Microsoft.Bcl.AsyncInterfaces/1.1.1": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-yuvf07qFWFqtK3P/MRkEKLhn5r2UbSpVueRziSqj0yJQIKFwG1pq9mOayK3zE5qZCTs0CbrwL9M6R8VwqyGy2w==",
+ "path": "microsoft.bcl.asyncinterfaces/1.1.1",
+ "hashPath": "microsoft.bcl.asyncinterfaces.1.1.1.nupkg.sha512"
+ },
+ "Microsoft.Bcl.HashCode/1.1.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-J2G1k+u5unBV+aYcwxo94ip16Rkp65pgWFb0R6zwJipzWNMgvqlWeuI7/+R+e8bob66LnSG+llLJ+z8wI94cHg==",
+ "path": "microsoft.bcl.hashcode/1.1.0",
+ "hashPath": "microsoft.bcl.hashcode.1.1.0.nupkg.sha512"
+ },
+ "Microsoft.Data.Sqlite/10.0.3": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-R/7g5hddFTzey+XOH7Hb3x71DdX5yDu3YqKDJbyn3QKiV6P38bYqkhsoDotanurh5mHLRc9hITE+pxL6Kjcpzw==",
+ "path": "microsoft.data.sqlite/10.0.3",
+ "hashPath": "microsoft.data.sqlite.10.0.3.nupkg.sha512"
+ },
+ "Microsoft.Data.Sqlite.Core/10.0.3": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-onD94qHlvteS2S9eg1T7Huehm3x92no4nU1AyDWjSmT6jDBhY8QF0UapwNhFA+1dArzxM10fV7s6uI1EIK2+dw==",
+ "path": "microsoft.data.sqlite.core/10.0.3",
+ "hashPath": "microsoft.data.sqlite.core.10.0.3.nupkg.sha512"
+ },
+ "Microsoft.EntityFrameworkCore/3.1.5": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-1jYVmK8dMKGhtMMrtw0hicRjAJq8hnFSuXHdJTIGa04UVWFvsMFwWsdO3Y1ziCLgR2xM7u5AgUcFLGbV0t9cOg==",
+ "path": "microsoft.entityframeworkcore/3.1.5",
+ "hashPath": "microsoft.entityframeworkcore.3.1.5.nupkg.sha512"
+ },
+ "Microsoft.EntityFrameworkCore.Abstractions/3.1.5": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-xZ3mUVu22p2h0ZKTWgoyK9YjA2H11cJcZssDcZYs8iLUVHPxMcb+ITOKpMdnV6SkEiQQ5pvjPXQ5J7VxiGSwDw==",
+ "path": "microsoft.entityframeworkcore.abstractions/3.1.5",
+ "hashPath": "microsoft.entityframeworkcore.abstractions.3.1.5.nupkg.sha512"
+ },
+ "Microsoft.Extensions.Caching.Abstractions/3.1.5": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-1HWdvlBNI4laVkx5oglv3Oaz5s8dO/dpkOep8FLv7+QAK2rm3ofBVv49aiDnijnwO+ZPJ/0iNctZWP0W2S07Rw==",
+ "path": "microsoft.extensions.caching.abstractions/3.1.5",
+ "hashPath": "microsoft.extensions.caching.abstractions.3.1.5.nupkg.sha512"
+ },
+ "Microsoft.Extensions.Caching.Memory/3.1.5": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-tqnVZ/tyXiBAEeLtcTvyb/poVp/gn6bbpdr12SAbO4TcfNkd1eNCEbyyABo0qhiQh6vVwba41aixklElx4grdw==",
+ "path": "microsoft.extensions.caching.memory/3.1.5",
+ "hashPath": "microsoft.extensions.caching.memory.3.1.5.nupkg.sha512"
+ },
+ "Microsoft.Extensions.Configuration/10.0.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-H4SWETCh/cC5L1WtWchHR6LntGk3rDTTznZMssr4cL8IbDmMWBxY+MOGDc/ASnqNolLKPIWHWeuC1ddiL/iNPw==",
+ "path": "microsoft.extensions.configuration/10.0.0",
+ "hashPath": "microsoft.extensions.configuration.10.0.0.nupkg.sha512"
+ },
+ "Microsoft.Extensions.Configuration.Abstractions/10.0.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-d2kDKnCsJvY7mBVhcjPSp9BkJk48DsaHPg5u+Oy4f8XaOqnEedRy/USyvnpHL92wpJ6DrTPy7htppUUzskbCXQ==",
+ "path": "microsoft.extensions.configuration.abstractions/10.0.0",
+ "hashPath": "microsoft.extensions.configuration.abstractions.10.0.0.nupkg.sha512"
+ },
+ "Microsoft.Extensions.Configuration.Binder/10.0.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-tMF9wNh+hlyYDWB8mrFCQHQmWHlRosol1b/N2Jrefy1bFLnuTlgSYmPyHNmz8xVQgs7DpXytBRWxGhG+mSTp0g==",
+ "path": "microsoft.extensions.configuration.binder/10.0.0",
+ "hashPath": "microsoft.extensions.configuration.binder.10.0.0.nupkg.sha512"
+ },
+ "Microsoft.Extensions.DependencyInjection/3.1.5": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-I+RTJQi7TtenIHZqL2zr6523PYXfL88Ruu4UIVmspIxdw14GHd8zZ+2dGLSdwX7fn41Hth4d42S1e1iHWVOJyQ==",
+ "path": "microsoft.extensions.dependencyinjection/3.1.5",
+ "hashPath": "microsoft.extensions.dependencyinjection.3.1.5.nupkg.sha512"
+ },
+ "Microsoft.Extensions.DependencyInjection.Abstractions/8.0.1": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-fGLiCRLMYd00JYpClraLjJTNKLmMJPnqxMaiRzEBIIvevlzxz33mXy39Lkd48hu1G+N21S7QpaO5ZzKsI6FRuA==",
+ "path": "microsoft.extensions.dependencyinjection.abstractions/8.0.1",
+ "hashPath": "microsoft.extensions.dependencyinjection.abstractions.8.0.1.nupkg.sha512"
+ },
+ "Microsoft.Extensions.DependencyModel/10.0.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-RFYJR7APio/BiqdQunRq6DB+nDB6nc2qhHr77mlvZ0q0BT8PubMXN7XicmfzCbrDE/dzhBnUKBRXLTcqUiZDGg==",
+ "path": "microsoft.extensions.dependencymodel/10.0.0",
+ "hashPath": "microsoft.extensions.dependencymodel.10.0.0.nupkg.sha512"
+ },
+ "Microsoft.Extensions.Logging/3.1.5": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-C85NYDym6xy03o70vxX+VQ4ZEjj5Eg5t5QoGW0t100vG5MmPL6+G3XXcQjIIn1WRQrjzGWzQwuKf38fxXEWIWA==",
+ "path": "microsoft.extensions.logging/3.1.5",
+ "hashPath": "microsoft.extensions.logging.3.1.5.nupkg.sha512"
+ },
+ "Microsoft.Extensions.Logging.Abstractions/3.1.5": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-ZvwowjRSWXewdPI+whPFXgwF4Qme6Q9KV9SCPEITSGiqHLArct7q5hTBtTzj3GPsVLjTqehvTg6Bd/EQk9JS0A==",
+ "path": "microsoft.extensions.logging.abstractions/3.1.5",
+ "hashPath": "microsoft.extensions.logging.abstractions.3.1.5.nupkg.sha512"
+ },
+ "Microsoft.Extensions.Options/3.1.5": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-f+JT/7lkKBMp/Ak2tVjO+TD7o+UoCfjnExkZNn0PZIso8kIXrqNy6x42Lrxf4Q0pW3JMf9ExmL2EQlvk2XnFAg==",
+ "path": "microsoft.extensions.options/3.1.5",
+ "hashPath": "microsoft.extensions.options.3.1.5.nupkg.sha512"
+ },
+ "Microsoft.Extensions.Primitives/10.0.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-inRnbpCS0nwO/RuoZIAqxQUuyjaknOOnCEZB55KSMMjRhl0RQDttSmLSGsUJN3RQ3ocf5NDLFd2mOQViHqMK5w==",
+ "path": "microsoft.extensions.primitives/10.0.0",
+ "hashPath": "microsoft.extensions.primitives.10.0.0.nupkg.sha512"
+ },
+ "Microsoft.OData.Client/7.8.3": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-7cff1FigrRIXejr6a9DamhjYz+2MwEBfe6WnAs4Pi0ljgFSPdvQyHc1ANNZJo/VMkufmekOTW6T9iE/0SNRJ9Q==",
+ "path": "microsoft.odata.client/7.8.3",
+ "hashPath": "microsoft.odata.client.7.8.3.nupkg.sha512"
+ },
+ "Microsoft.OData.Core/7.8.3": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-rQ3u7Lw3RGWEfci3En1HzzU6jh/8nRDH6Q013QPAh8z7A/UUAuo8qf/5/8D9Mi6yybJVpUHVeYwREuzzpojD9w==",
+ "path": "microsoft.odata.core/7.8.3",
+ "hashPath": "microsoft.odata.core.7.8.3.nupkg.sha512"
+ },
+ "Microsoft.OData.Edm/7.8.3": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-/FvQD76LzScuc350wyiESY9UE3aSgBLXT16AKutwvHPO9c4bIaTPPf3JU/cqtXTDz2632nyIUzzSaccBadduEA==",
+ "path": "microsoft.odata.edm/7.8.3",
+ "hashPath": "microsoft.odata.edm.7.8.3.nupkg.sha512"
+ },
+ "Microsoft.Spatial/7.8.3": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-hFKWwu2TWng9+Ye8j/zIiaFBbRGysplZ/HvSYCfnrXxQ/bl3zIsLRxUgACLcPAoJMhQDeLaRwHYvQGPiJhusoQ==",
+ "path": "microsoft.spatial/7.8.3",
+ "hashPath": "microsoft.spatial.7.8.3.nupkg.sha512"
+ },
+ "Microsoft.Xaml.Behaviors.Wpf/1.1.122": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-SgcafT189u4qX++vSCV9FLQ4BsRXU9J2esnHA9IF8GOSgnPBulFw1CW4X/FYoOXvZwdDZxlSObJUGUg1U1wSyg==",
+ "path": "microsoft.xaml.behaviors.wpf/1.1.122",
+ "hashPath": "microsoft.xaml.behaviors.wpf.1.1.122.nupkg.sha512"
+ },
+ "Prism.Container.Abstractions/9.0.106": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-QNOERNOqsxvAa8pbWjqFB872DkvYK/cVRrcFO5vJYgWTIKBd8xfaI/jaZ0qeXLYVDz0nrvgJTZVVnip6+68dCw==",
+ "path": "prism.container.abstractions/9.0.106",
+ "hashPath": "prism.container.abstractions.9.0.106.nupkg.sha512"
+ },
+ "Prism.Core/9.0.537": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-D7mEqPKLVNrD0g2WHCpC/MOKwn8h7X1liCWyjqjL7NCuxgwuhVLTG85E/ZPBkISrXdwvOQZ+bSY31bvP79FQlg==",
+ "path": "prism.core/9.0.537",
+ "hashPath": "prism.core.9.0.537.nupkg.sha512"
+ },
+ "Prism.Events/9.0.537": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-Pzp5MGUuhAyKXZUbHVYNWLGF/eA3sScqDN6VrzbWlKj85R0IS0q+JXe99umynso2xhXAe+1jrQCCkgqmEFCBng==",
+ "path": "prism.events/9.0.537",
+ "hashPath": "prism.events.9.0.537.nupkg.sha512"
+ },
+ "Prism.Wpf/9.0.537": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-srsXhi7FRUFawsNoRkY67duMEGjZo3ff0FpqpkjeWkkAuLazlH1UmNVrvwnpaLQCBboexH/z6oGrLvpeocxgdw==",
+ "path": "prism.wpf/9.0.537",
+ "hashPath": "prism.wpf.9.0.537.nupkg.sha512"
+ },
+ "Serilog/4.3.1": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-savYe7h5yRlkqBVOwP8cIRDOdqKiPmYCU4W87JH38sBmcKD5EBoXvQIw6bNEvZ/pTe1gsiye3VFCzBsoppGkXQ==",
+ "path": "serilog/4.3.1",
+ "hashPath": "serilog.4.3.1.nupkg.sha512"
+ },
+ "Serilog.Settings.Configuration/10.0.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==",
+ "path": "serilog.settings.configuration/10.0.0",
+ "hashPath": "serilog.settings.configuration.10.0.0.nupkg.sha512"
+ },
+ "Serilog.Sinks.Console/6.1.1": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==",
+ "path": "serilog.sinks.console/6.1.1",
+ "hashPath": "serilog.sinks.console.6.1.1.nupkg.sha512"
+ },
+ "Serilog.Sinks.File/7.0.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==",
+ "path": "serilog.sinks.file/7.0.0",
+ "hashPath": "serilog.sinks.file.7.0.0.nupkg.sha512"
+ },
+ "SharpDX/4.2.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-3pv0LFMvfK/dv1qISJnn8xBeeT6R/FRvr0EV4KI2DGsL84Qlv6P7isWqxGyU0LCwlSVCJN3jgHJ4Bl0KI2PJww==",
+ "path": "sharpdx/4.2.0",
+ "hashPath": "sharpdx.4.2.0.nupkg.sha512"
+ },
+ "SharpDX.D3DCompiler/4.2.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-Rnsd6Ilp127xbXqhTit8WKFQUrXwWxqVGpglyWDNkIBCk0tWXNQEjrJpsl0KAObzyZaa33+EXAikLVt5fnd3GA==",
+ "path": "sharpdx.d3dcompiler/4.2.0",
+ "hashPath": "sharpdx.d3dcompiler.4.2.0.nupkg.sha512"
+ },
+ "SharpDX.Direct2D1/4.2.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-Qs8LzDMaQf1u3KB8ArHu9pDv6itZ++QXs99a/bVAG+nKr0Hx5NG4mcN5vsfE0mVR2TkeHfeUm4PksRah6VUPtA==",
+ "path": "sharpdx.direct2d1/4.2.0",
+ "hashPath": "sharpdx.direct2d1.4.2.0.nupkg.sha512"
+ },
+ "SharpDX.Direct3D10/4.2.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-SxWfe7L0MJIs9VvsesmPz4w0EIJBlYZ4oNJsBeSntumbN7MslqKVxt3fiWLrGyGq0zmb7mLWQhzhnV0we4IW9Q==",
+ "path": "sharpdx.direct3d10/4.2.0",
+ "hashPath": "sharpdx.direct3d10.4.2.0.nupkg.sha512"
+ },
+ "SharpDX.Direct3D9/4.2.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-1KgqXvPj/j5hLYxfcOQoJdHskamu7EWg60n9LBAYhun7dR+sKS0ZDNb7NuKwFP1bM0HZqvokjXQIf3oA2NpdqA==",
+ "path": "sharpdx.direct3d9/4.2.0",
+ "hashPath": "sharpdx.direct3d9.4.2.0.nupkg.sha512"
+ },
+ "SharpDX.DXGI/4.2.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-UjKqkgWc8U+SP+j3LBzFP6OB6Ntapjih7Xo+g1rLcsGbIb5KwewBrBChaUu7sil8rWoeVU/k0EJd3SMN4VqNZw==",
+ "path": "sharpdx.dxgi/4.2.0",
+ "hashPath": "sharpdx.dxgi.4.2.0.nupkg.sha512"
+ },
+ "SharpDX.Mathematics/4.2.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-R2pcKLgdsP9p5WyTjHmGOZ0ka0zASAZYc6P4L6rSvjYhf6klGYbent7MiVwbkwkt9dD44p5brjy5IwAnVONWGw==",
+ "path": "sharpdx.mathematics/4.2.0",
+ "hashPath": "sharpdx.mathematics.4.2.0.nupkg.sha512"
+ },
+ "SQLitePCLRaw.bundle_e_sqlite3/2.1.11": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-DC4nA7yWnf4UZdgJDF+9Mus4/cb0Y3Sfgi3gDnAoKNAIBwzkskNAbNbyu+u4atT0ruVlZNJfwZmwiEwE5oz9LQ==",
+ "path": "sqlitepclraw.bundle_e_sqlite3/2.1.11",
+ "hashPath": "sqlitepclraw.bundle_e_sqlite3.2.1.11.nupkg.sha512"
+ },
+ "SQLitePCLRaw.core/2.1.11": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-PK0GLFkfhZzLQeR3PJf71FmhtHox+U3vcY6ZtswoMjrefkB9k6ErNJEnwXqc5KgXDSjige2XXrezqS39gkpQKA==",
+ "path": "sqlitepclraw.core/2.1.11",
+ "hashPath": "sqlitepclraw.core.2.1.11.nupkg.sha512"
+ },
+ "SQLitePCLRaw.lib.e_sqlite3/2.1.11": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-Ev2ytaXiOlWZ4b3R67GZBsemTINslLD1DCJr2xiacpn4tbapu0Q4dHEzSvZSMnVWeE5nlObU3VZN2p81q3XOYQ==",
+ "path": "sqlitepclraw.lib.e_sqlite3/2.1.11",
+ "hashPath": "sqlitepclraw.lib.e_sqlite3.2.1.11.nupkg.sha512"
+ },
+ "SQLitePCLRaw.provider.e_sqlite3/2.1.11": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-Y/0ZkR+r0Cg3DQFuCl1RBnv/tmxpIZRU3HUvelPw6MVaKHwYYR8YNvgs0vuNuXCMvlyJ+Fh88U1D4tah1tt6qw==",
+ "path": "sqlitepclraw.provider.e_sqlite3/2.1.11",
+ "hashPath": "sqlitepclraw.provider.e_sqlite3.2.1.11.nupkg.sha512"
+ },
+ "System.Data.OleDb/6.0.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-LQ8PjTIF1LtrrlGiyiTVjAkQtTWKm9GSNnygIlWjhN9y88s7xhy6DUNDDkmQQ9f6ex7mA4k0Tl97lz/CklaiLg==",
+ "path": "system.data.oledb/6.0.0",
+ "hashPath": "system.data.oledb.6.0.0.nupkg.sha512"
+ },
+ "System.IO.Pipelines/10.0.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-M1eb3nfXntaRJPrrMVM9EFS8I1bDTnt0uvUS6QP/SicZf/ZZjydMD5NiXxfmwW/uQwaMDP/yX2P+zQN1NBHChg==",
+ "path": "system.io.pipelines/10.0.0",
+ "hashPath": "system.io.pipelines.10.0.0.nupkg.sha512"
+ },
+ "System.Private.ServiceModel/4.7.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-BItrYCkoTV3VzVPsrew+uc34fmLb+3ncgspa7vbO3vkfY9JQCea4u34pHE+Bcv1Iy16MgRs3n2jKVRCDg0rPfg==",
+ "path": "system.private.servicemodel/4.7.0",
+ "hashPath": "system.private.servicemodel.4.7.0.nupkg.sha512"
+ },
+ "System.ServiceModel.Http/4.7.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-+BB61ycl1cSlRbJDpABoqMa7bRE4boJfK1CfWfbNzTGeADFVmDkhylpfmC1bKloxtf95p2owj8/n7kilgRBAow==",
+ "path": "system.servicemodel.http/4.7.0",
+ "hashPath": "system.servicemodel.http.4.7.0.nupkg.sha512"
+ },
+ "System.ServiceModel.Primitives/4.7.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-YUXIMO4kL1v6dUVptJGixAx/8Ai5trQzVn3gbk0mpwxh77kGAs+MyBRoHN/5ZoxtwNn4E1dq3N4rJCAgAUaiJA==",
+ "path": "system.servicemodel.primitives/4.7.0",
+ "hashPath": "system.servicemodel.primitives.4.7.0.nupkg.sha512"
+ },
+ "System.Text.Encodings.Web/10.0.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-257hh1ep1Gqm1Lm0ulxf7vVBVMJuGN6EL4xSWjpi46DffXzm1058IiWsfSC06zSm7SniN+Tb5160UnXsSa8rRg==",
+ "path": "system.text.encodings.web/10.0.0",
+ "hashPath": "system.text.encodings.web.10.0.0.nupkg.sha512"
+ },
+ "System.Text.Json/10.0.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-1Dpjwq9peG/Wt5BNbrzIhTpclfOSqBWZsUO28vVr59yQlkvL5jLBWfpfzRmJ1OY+6DciaY0DUcltyzs4fuZHjw==",
+ "path": "system.text.json/10.0.0",
+ "hashPath": "system.text.json.10.0.0.nupkg.sha512"
+ },
+ "Telerik.UI.for.Wpf.NetCore.Xaml/2024.1.408": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-vf5CWl51zh5KwdkNZyqOIHfabaTeYwxSxrb9J0NN21/kOwudt+qezzB/BpwksOvo4Yyy8tvGZctFW1nG8meG+g==",
+ "path": "telerik.ui.for.wpf.netcore.xaml/2024.1.408",
+ "hashPath": "telerik.ui.for.wpf.netcore.xaml.2024.1.408.nupkg.sha512"
+ }
+ }
+}
\ No newline at end of file
diff --git a/XP.Common/ReleaseFiles/XP.Common.dll b/XP.Common/ReleaseFiles/XP.Common.dll
new file mode 100644
index 0000000..771fee5
Binary files /dev/null and b/XP.Common/ReleaseFiles/XP.Common.dll differ
diff --git a/XP.Common/ReleaseFiles/XP.Common.pdb b/XP.Common/ReleaseFiles/XP.Common.pdb
new file mode 100644
index 0000000..07bc803
Binary files /dev/null and b/XP.Common/ReleaseFiles/XP.Common.pdb differ
diff --git a/XP.Common/ReleaseFiles/en-US/XP.Common.resources.dll b/XP.Common/ReleaseFiles/en-US/XP.Common.resources.dll
new file mode 100644
index 0000000..ca9d357
Binary files /dev/null and b/XP.Common/ReleaseFiles/en-US/XP.Common.resources.dll differ
diff --git a/XP.Common/ReleaseFiles/zh-CN/XP.Common.resources.dll b/XP.Common/ReleaseFiles/zh-CN/XP.Common.resources.dll
new file mode 100644
index 0000000..abdb513
Binary files /dev/null and b/XP.Common/ReleaseFiles/zh-CN/XP.Common.resources.dll differ
diff --git a/XP.Common/ReleaseFiles/zh-TW/XP.Common.resources.dll b/XP.Common/ReleaseFiles/zh-TW/XP.Common.resources.dll
new file mode 100644
index 0000000..03a3123
Binary files /dev/null and b/XP.Common/ReleaseFiles/zh-TW/XP.Common.resources.dll differ
diff --git a/XP.Common/Resources/Resources.Designer.cs b/XP.Common/Resources/Resources.Designer.cs
new file mode 100644
index 0000000..01a5689
--- /dev/null
+++ b/XP.Common/Resources/Resources.Designer.cs
@@ -0,0 +1,1497 @@
+//------------------------------------------------------------------------------
+//
+// 此代码由工具生成。
+// 运行时版本:4.0.30319.42000
+//
+// 对此文件的更改可能会导致不正确的行为,并且如果
+// 重新生成代码,这些更改将会丢失。
+//
+//------------------------------------------------------------------------------
+
+namespace XP.Common.Resources {
+ using System;
+
+
+ ///
+ /// 一个强类型的资源类,用于查找本地化的字符串等。
+ ///
+ // 此类是由 StronglyTypedResourceBuilder
+ // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
+ // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
+ // (以 /str 作为命令选项),或重新生成 VS 项目。
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ public class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// 返回此类使用的缓存的 ResourceManager 实例。
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("XP.Common.Resources.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// 重写当前线程的 CurrentUICulture 属性,对
+ /// 使用此强类型资源类的所有资源查找执行重写。
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// 查找类似 XplorePlane X射线检测系统 的本地化字符串。
+ ///
+ public static string App_Title {
+ get {
+ return ResourceManager.GetString("App_Title", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 添加 的本地化字符串。
+ ///
+ public static string Button_Add {
+ get {
+ return ResourceManager.GetString("Button_Add", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 应用 的本地化字符串。
+ ///
+ public static string Button_Apply {
+ get {
+ return ResourceManager.GetString("Button_Apply", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 取消 的本地化字符串。
+ ///
+ public static string Button_Cancel {
+ get {
+ return ResourceManager.GetString("Button_Cancel", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 关闭 的本地化字符串。
+ ///
+ public static string Button_Close {
+ get {
+ return ResourceManager.GetString("Button_Close", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 删除 的本地化字符串。
+ ///
+ public static string Button_Delete {
+ get {
+ return ResourceManager.GetString("Button_Delete", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 编辑 的本地化字符串。
+ ///
+ public static string Button_Edit {
+ get {
+ return ResourceManager.GetString("Button_Edit", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 确定 的本地化字符串。
+ ///
+ public static string Button_OK {
+ get {
+ return ResourceManager.GetString("Button_OK", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 刷新 的本地化字符串。
+ ///
+ public static string Button_Refresh {
+ get {
+ return ResourceManager.GetString("Button_Refresh", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 重置 的本地化字符串。
+ ///
+ public static string Button_Reset {
+ get {
+ return ResourceManager.GetString("Button_Reset", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 保存 的本地化字符串。
+ ///
+ public static string Button_Save {
+ get {
+ return ResourceManager.GetString("Button_Save", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 搜索 的本地化字符串。
+ ///
+ public static string Button_Search {
+ get {
+ return ResourceManager.GetString("Button_Search", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 帧合并 的本地化字符串。
+ ///
+ public static string Detector_AvgFramesLabel {
+ get {
+ return ResourceManager.GetString("Detector_AvgFramesLabel", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 像素合并 的本地化字符串。
+ ///
+ public static string Detector_BinningLabel {
+ get {
+ return ResourceManager.GetString("Detector_BinningLabel", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 探测器配置 的本地化字符串。
+ ///
+ public static string Detector_ConfigWindowTitle {
+ get {
+ return ResourceManager.GetString("Detector_ConfigWindowTitle", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 探测器连接失败 的本地化字符串。
+ ///
+ public static string Detector_ConnectFailed {
+ get {
+ return ResourceManager.GetString("Detector_ConnectFailed", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 探测器连接成功 的本地化字符串。
+ ///
+ public static string Detector_ConnectSuccess {
+ get {
+ return ResourceManager.GetString("Detector_ConnectSuccess", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 暗场校正 的本地化字符串。
+ ///
+ public static string Detector_DarkCorrectionButton {
+ get {
+ return ResourceManager.GetString("Detector_DarkCorrectionButton", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 探测器断开成功 的本地化字符串。
+ ///
+ public static string Detector_DisconnectSuccess {
+ get {
+ return ResourceManager.GetString("Detector_DisconnectSuccess", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 帧率 的本地化字符串。
+ ///
+ public static string Detector_FrameRateLabel {
+ get {
+ return ResourceManager.GetString("Detector_FrameRateLabel", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 亮场校正 的本地化字符串。
+ ///
+ public static string Detector_LightCorrectionButton {
+ get {
+ return ResourceManager.GetString("Detector_LightCorrectionButton", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 灵敏度 的本地化字符串。
+ ///
+ public static string Detector_SensitivityLabel {
+ get {
+ return ResourceManager.GetString("Detector_SensitivityLabel", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 采集中 的本地化字符串。
+ ///
+ public static string Detector_Status_Acquiring {
+ get {
+ return ResourceManager.GetString("Detector_Status_Acquiring", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 探测器控制 的本地化字符串。
+ ///
+ public static string Detector_Title {
+ get {
+ return ResourceManager.GetString("Detector_Title", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 确认 的本地化字符串。
+ ///
+ public static string Dialog_Confirmation {
+ get {
+ return ResourceManager.GetString("Dialog_Confirmation", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 错误 的本地化字符串。
+ ///
+ public static string Dialog_Error {
+ get {
+ return ResourceManager.GetString("Dialog_Error", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 信息 的本地化字符串。
+ ///
+ public static string Dialog_Information {
+ get {
+ return ResourceManager.GetString("Dialog_Information", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 提示 的本地化字符串。
+ ///
+ public static string Dialog_Notice {
+ get {
+ return ResourceManager.GetString("Dialog_Notice", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 警告 的本地化字符串。
+ ///
+ public static string Dialog_Warning {
+ get {
+ return ResourceManager.GetString("Dialog_Warning", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 English 的本地化字符串。
+ ///
+ public static string Language_EnUS {
+ get {
+ return ResourceManager.GetString("Language_EnUS", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 简体中文 的本地化字符串。
+ ///
+ public static string Language_ZhCN {
+ get {
+ return ResourceManager.GetString("Language_ZhCN", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 繁體中文 的本地化字符串。
+ ///
+ public static string Language_ZhTW {
+ get {
+ return ResourceManager.GetString("Language_ZhTW", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 关于 的本地化字符串。
+ ///
+ public static string Menu_About {
+ get {
+ return ResourceManager.GetString("Menu_About", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 退出 的本地化字符串。
+ ///
+ public static string Menu_Exit {
+ get {
+ return ResourceManager.GetString("Menu_Exit", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 文件 的本地化字符串。
+ ///
+ public static string Menu_File {
+ get {
+ return ResourceManager.GetString("Menu_File", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 帮助 的本地化字符串。
+ ///
+ public static string Menu_Help {
+ get {
+ return ResourceManager.GetString("Menu_Help", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 设置 的本地化字符串。
+ ///
+ public static string Menu_Settings {
+ get {
+ return ResourceManager.GetString("Menu_Settings", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 工具 的本地化字符串。
+ ///
+ public static string Menu_Tools {
+ get {
+ return ResourceManager.GetString("Menu_Tools", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 视图 的本地化字符串。
+ ///
+ public static string Menu_View {
+ get {
+ return ResourceManager.GetString("Menu_View", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 确定要删除吗? 的本地化字符串。
+ ///
+ public static string Message_ConfirmDelete {
+ get {
+ return ResourceManager.GetString("Message_ConfirmDelete", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 连接已断开 的本地化字符串。
+ ///
+ public static string Message_ConnectionLost {
+ get {
+ return ResourceManager.GetString("Message_ConnectionLost", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 输入无效,请检查后重试 的本地化字符串。
+ ///
+ public static string Message_InvalidInput {
+ get {
+ return ResourceManager.GetString("Message_InvalidInput", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 操作失败 的本地化字符串。
+ ///
+ public static string Message_OperationFailed {
+ get {
+ return ResourceManager.GetString("Message_OperationFailed", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 操作成功完成 的本地化字符串。
+ ///
+ public static string Message_OperationSuccess {
+ get {
+ return ResourceManager.GetString("Message_OperationSuccess", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 语言设置已保存,请重启应用程序以应用新语言。 的本地化字符串。
+ ///
+ public static string Message_RestartRequired {
+ get {
+ return ResourceManager.GetString("Message_RestartRequired", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 有未保存的更改,是否保存? 的本地化字符串。
+ ///
+ public static string Message_UnsavedChanges {
+ get {
+ return ResourceManager.GetString("Message_UnsavedChanges", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 PLC 连接失败 的本地化字符串。
+ ///
+ public static string PLC_ConnectFailed {
+ get {
+ return ResourceManager.GetString("PLC_ConnectFailed", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 PLC 连接成功 的本地化字符串。
+ ///
+ public static string PLC_ConnectSuccess {
+ get {
+ return ResourceManager.GetString("PLC_ConnectSuccess", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 PLC 断开成功 的本地化字符串。
+ ///
+ public static string PLC_DisconnectSuccess {
+ get {
+ return ResourceManager.GetString("PLC_DisconnectSuccess", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 变量读取成功 的本地化字符串。
+ ///
+ public static string PLC_ReadVariableSuccess {
+ get {
+ return ResourceManager.GetString("PLC_ReadVariableSuccess", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 PLC 控制 的本地化字符串。
+ ///
+ public static string PLC_Title {
+ get {
+ return ResourceManager.GetString("PLC_Title", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 变量写入成功 的本地化字符串。
+ ///
+ public static string PLC_WriteVariableSuccess {
+ get {
+ return ResourceManager.GetString("PLC_WriteVariableSuccess", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 实际 {0} 的本地化字符串。
+ ///
+ public static string RaySource_ActualValueLabel {
+ get {
+ return ResourceManager.GetString("RaySource_ActualValueLabel", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 高级 的本地化字符串。
+ ///
+ public static string RaySource_AdvanceButton {
+ get {
+ return ResourceManager.GetString("RaySource_AdvanceButton", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 是否确认执行全部电压自动定心操作? 的本地化字符串。
+ ///
+ public static string RaySource_AutoCenter_Confirm {
+ get {
+ return ResourceManager.GetString("RaySource_AutoCenter_Confirm", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 正在执行全部电压自动定心操作,请稍候... 的本地化字符串。
+ ///
+ public static string RaySource_AutoCenter_Message {
+ get {
+ return ResourceManager.GetString("RaySource_AutoCenter_Message", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 自动定心 的本地化字符串。
+ ///
+ public static string RaySource_AutoCenter_Title {
+ get {
+ return ResourceManager.GetString("RaySource_AutoCenter_Title", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 自动定心: 的本地化字符串。
+ ///
+ public static string RaySource_AutoCenterLabel {
+ get {
+ return ResourceManager.GetString("RaySource_AutoCenterLabel", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 自动定心 的本地化字符串。
+ ///
+ public static string RaySource_AutoCenterSettingButton {
+ get {
+ return ResourceManager.GetString("RaySource_AutoCenterSettingButton", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 配置 的本地化字符串。
+ ///
+ public static string RaySource_ConfigButton {
+ get {
+ return ResourceManager.GetString("RaySource_ConfigButton", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 射线源配置 的本地化字符串。
+ ///
+ public static string RaySource_ConfigWindowTitle {
+ get {
+ return ResourceManager.GetString("RaySource_ConfigWindowTitle", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 确认 的本地化字符串。
+ ///
+ public static string RaySource_Confirm_Title {
+ get {
+ return ResourceManager.GetString("RaySource_Confirm_Title", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 连接射线源 的本地化字符串。
+ ///
+ public static string RaySource_ConnectButton {
+ get {
+ return ResourceManager.GetString("RaySource_ConnectButton", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 已连接 的本地化字符串。
+ ///
+ public static string RaySource_Connected {
+ get {
+ return ResourceManager.GetString("RaySource_Connected", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 连接射线源设备 的本地化字符串。
+ ///
+ public static string RaySource_ConnectTooltip {
+ get {
+ return ResourceManager.GetString("RaySource_ConnectTooltip", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 连接变量 的本地化字符串。
+ ///
+ public static string RaySource_ConnectVariablesButton {
+ get {
+ return ResourceManager.GetString("RaySource_ConnectVariablesButton", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 电流 (μA) 的本地化字符串。
+ ///
+ public static string RaySource_CurrentLabel {
+ get {
+ return ResourceManager.GetString("RaySource_CurrentLabel", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 断开射线源 的本地化字符串。
+ ///
+ public static string RaySource_DisconnectButton {
+ get {
+ return ResourceManager.GetString("RaySource_DisconnectButton", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 未连接 的本地化字符串。
+ ///
+ public static string RaySource_Disconnected {
+ get {
+ return ResourceManager.GetString("RaySource_Disconnected", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 断开射线源设备 的本地化字符串。
+ ///
+ public static string RaySource_DisconnectTooltip {
+ get {
+ return ResourceManager.GetString("RaySource_DisconnectTooltip", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 X射线源紧急停止 的本地化字符串。
+ ///
+ public static string RaySource_EmergencyStop {
+ get {
+ return ResourceManager.GetString("RaySource_EmergencyStop", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 射线开启时无法调整参数 的本地化字符串。
+ ///
+ public static string RaySource_Error_CannotAdjustWhileXrayOn {
+ get {
+ return ResourceManager.GetString("RaySource_Error_CannotAdjustWhileXrayOn", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 电流超出范围 (10-1000 μA) 的本地化字符串。
+ ///
+ public static string RaySource_Error_CurrentOutOfRange {
+ get {
+ return ResourceManager.GetString("RaySource_Error_CurrentOutOfRange", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 电压超出范围 (20-225 kV) 的本地化字符串。
+ ///
+ public static string RaySource_Error_VoltageOutOfRange {
+ get {
+ return ResourceManager.GetString("RaySource_Error_VoltageOutOfRange", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 是否确认执行灯丝校准操作? 的本地化字符串。
+ ///
+ public static string RaySource_FilamentCalibration_Confirm {
+ get {
+ return ResourceManager.GetString("RaySource_FilamentCalibration_Confirm", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 正在执行灯丝校准操作,请稍候... 的本地化字符串。
+ ///
+ public static string RaySource_FilamentCalibration_Message {
+ get {
+ return ResourceManager.GetString("RaySource_FilamentCalibration_Message", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 正在执行灯丝校准操作,请稍候... 的本地化字符串。
+ ///
+ public static string RaySource_FilamentCalibration_Title {
+ get {
+ return ResourceManager.GetString("RaySource_FilamentCalibration_Title", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 灯丝校准 的本地化字符串。
+ ///
+ public static string RaySource_FilamentCalibrationButton {
+ get {
+ return ResourceManager.GetString("RaySource_FilamentCalibrationButton", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 灯丝校准: 的本地化字符串。
+ ///
+ public static string RaySource_FilamentLabel {
+ get {
+ return ResourceManager.GetString("RaySource_FilamentLabel", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 灯丝寿命: 的本地化字符串。
+ ///
+ public static string RaySource_FilamentLifetimeLabel {
+ get {
+ return ResourceManager.GetString("RaySource_FilamentLifetimeLabel", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 确认 的本地化字符串。
+ ///
+ public static string RaySource_FilamentLifetimeWarningConfirm {
+ get {
+ return ResourceManager.GetString("RaySource_FilamentLifetimeWarningConfirm", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 灯丝累计使用 {0} 小时,寿命阈值 {1} 小时,已使用 {2}%。建议尽快更换灯丝。 的本地化字符串。
+ ///
+ public static string RaySource_FilamentLifetimeWarningMessage {
+ get {
+ return ResourceManager.GetString("RaySource_FilamentLifetimeWarningMessage", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 灯丝寿命预警 的本地化字符串。
+ ///
+ public static string RaySource_FilamentLifetimeWarningTitle {
+ get {
+ return ResourceManager.GetString("RaySource_FilamentLifetimeWarningTitle", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 High Power 的本地化字符串。
+ ///
+ public static string RaySource_HighPowerButton {
+ get {
+ return ResourceManager.GetString("RaySource_HighPowerButton", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 X射线源初始化失败 的本地化字符串。
+ ///
+ public static string RaySource_InitFailed {
+ get {
+ return ResourceManager.GetString("RaySource_InitFailed", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 初始化 的本地化字符串。
+ ///
+ public static string RaySource_InitializeButton {
+ get {
+ return ResourceManager.GetString("RaySource_InitializeButton", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 X射线源初始化成功 的本地化字符串。
+ ///
+ public static string RaySource_InitSuccess {
+ get {
+ return ResourceManager.GetString("RaySource_InitSuccess", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 激活 的本地化字符串。
+ ///
+ public static string RaySource_InterlockActive {
+ get {
+ return ResourceManager.GetString("RaySource_InterlockActive", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 连锁: 的本地化字符串。
+ ///
+ public static string RaySource_InterlockLabel {
+ get {
+ return ResourceManager.GetString("RaySource_InterlockLabel", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 正常 的本地化字符串。
+ ///
+ public static string RaySource_InterlockNormal {
+ get {
+ return ResourceManager.GetString("RaySource_InterlockNormal", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 最大值 的本地化字符串。
+ ///
+ public static string RaySource_MaxValueLabel {
+ get {
+ return ResourceManager.GetString("RaySource_MaxValueLabel", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 Micro Focus 的本地化字符串。
+ ///
+ public static string RaySource_MicroFocusButton {
+ get {
+ return ResourceManager.GetString("RaySource_MicroFocusButton", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 最小值 的本地化字符串。
+ ///
+ public static string RaySource_MinValueLabel {
+ get {
+ return ResourceManager.GetString("RaySource_MinValueLabel", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 X射线源操作 的本地化字符串。
+ ///
+ public static string RaySource_OperateWindowTitle {
+ get {
+ return ResourceManager.GetString("RaySource_OperateWindowTitle", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 操作指令已发送,等待设备执行... 的本地化字符串。
+ ///
+ public static string RaySource_Operation_Sent {
+ get {
+ return ResourceManager.GetString("RaySource_Operation_Sent", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 功率 (W) 的本地化字符串。
+ ///
+ public static string RaySource_PowerLabel {
+ get {
+ return ResourceManager.GetString("RaySource_PowerLabel", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 功率模式: 的本地化字符串。
+ ///
+ public static string RaySource_PowerModeLabel {
+ get {
+ return ResourceManager.GetString("RaySource_PowerModeLabel", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 设置 的本地化字符串。
+ ///
+ public static string RaySource_SettingsButton {
+ get {
+ return ResourceManager.GetString("RaySource_SettingsButton", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 射线源类型: 的本地化字符串。
+ ///
+ public static string RaySource_SourceTypeLabel {
+ get {
+ return ResourceManager.GetString("RaySource_SourceTypeLabel", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 X射线源启动失败 的本地化字符串。
+ ///
+ public static string RaySource_StartFailed {
+ get {
+ return ResourceManager.GetString("RaySource_StartFailed", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 X射线源启动成功 的本地化字符串。
+ ///
+ public static string RaySource_StartSuccess {
+ get {
+ return ResourceManager.GetString("RaySource_StartSuccess", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 启动: 的本地化字符串。
+ ///
+ public static string RaySource_StartUpLabel {
+ get {
+ return ResourceManager.GetString("RaySource_StartUpLabel", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 错误 的本地化字符串。
+ ///
+ public static string RaySource_Status_Error {
+ get {
+ return ResourceManager.GetString("RaySource_Status_Error", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 故障 的本地化字符串。
+ ///
+ public static string RaySource_Status_Fault {
+ get {
+ return ResourceManager.GetString("RaySource_Status_Fault", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 空闲 的本地化字符串。
+ ///
+ public static string RaySource_Status_Idle {
+ get {
+ return ResourceManager.GetString("RaySource_Status_Idle", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 初始化中 的本地化字符串。
+ ///
+ public static string RaySource_Status_Initializing {
+ get {
+ return ResourceManager.GetString("RaySource_Status_Initializing", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 就绪 的本地化字符串。
+ ///
+ public static string RaySource_Status_Ready {
+ get {
+ return ResourceManager.GetString("RaySource_Status_Ready", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 预热中 的本地化字符串。
+ ///
+ public static string RaySource_Status_Warmup {
+ get {
+ return ResourceManager.GetString("RaySource_Status_Warmup", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 射线开启 的本地化字符串。
+ ///
+ public static string RaySource_Status_XrayOn {
+ get {
+ return ResourceManager.GetString("RaySource_Status_XrayOn", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 射线源
+ ///已关闭 的本地化字符串。
+ ///
+ public static string RaySource_StatusClosed {
+ get {
+ return ResourceManager.GetString("RaySource_StatusClosed", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 状态 的本地化字符串。
+ ///
+ public static string RaySource_StatusLabel {
+ get {
+ return ResourceManager.GetString("RaySource_StatusLabel", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 射线源
+ ///已开启 的本地化字符串。
+ ///
+ public static string RaySource_StatusOpened {
+ get {
+ return ResourceManager.GetString("RaySource_StatusOpened", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 射线源
+ ///不可用 的本地化字符串。
+ ///
+ public static string RaySource_StatusUnavailable {
+ get {
+ return ResourceManager.GetString("RaySource_StatusUnavailable", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 X射线源关闭失败 的本地化字符串。
+ ///
+ public static string RaySource_StopFailed {
+ get {
+ return ResourceManager.GetString("RaySource_StopFailed", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 X射线源关闭成功 的本地化字符串。
+ ///
+ public static string RaySource_StopSuccess {
+ get {
+ return ResourceManager.GetString("RaySource_StopSuccess", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 系统 的本地化字符串。
+ ///
+ public static string RaySource_SystemButton {
+ get {
+ return ResourceManager.GetString("RaySource_SystemButton", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 温度 (°C) 的本地化字符串。
+ ///
+ public static string RaySource_TemperatureLabel {
+ get {
+ return ResourceManager.GetString("RaySource_TemperatureLabel", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 X射线源控制 的本地化字符串。
+ ///
+ public static string RaySource_Title {
+ get {
+ return ResourceManager.GetString("RaySource_Title", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 是否确认执行训机操作? 的本地化字符串。
+ ///
+ public static string RaySource_Training_Confirm {
+ get {
+ return ResourceManager.GetString("RaySource_Training_Confirm", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 正在执行训机操作,请稍候... 的本地化字符串。
+ ///
+ public static string RaySource_Training_Message {
+ get {
+ return ResourceManager.GetString("RaySource_Training_Message", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 训机设置 的本地化字符串。
+ ///
+ public static string RaySource_Training_Title {
+ get {
+ return ResourceManager.GetString("RaySource_Training_Title", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 训机设置 的本地化字符串。
+ ///
+ public static string RaySource_TrainingSettingButton {
+ get {
+ return ResourceManager.GetString("RaySource_TrainingSettingButton", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 关闭射线源 的本地化字符串。
+ ///
+ public static string RaySource_TurnOffButton {
+ get {
+ return ResourceManager.GetString("RaySource_TurnOffButton", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 开启射线源 的本地化字符串。
+ ///
+ public static string RaySource_TurnOnButton {
+ get {
+ return ResourceManager.GetString("RaySource_TurnOnButton", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 TXI OFF 的本地化字符串。
+ ///
+ public static string RaySource_TxiOffButton {
+ get {
+ return ResourceManager.GetString("RaySource_TxiOffButton", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 TXI ON 的本地化字符串。
+ ///
+ public static string RaySource_TxiOnButton {
+ get {
+ return ResourceManager.GetString("RaySource_TxiOnButton", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 TXI状态: 的本地化字符串。
+ ///
+ public static string RaySource_TxiStatusLabel {
+ get {
+ return ResourceManager.GetString("RaySource_TxiStatusLabel", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 真空度 的本地化字符串。
+ ///
+ public static string RaySource_VacuumLabel {
+ get {
+ return ResourceManager.GetString("RaySource_VacuumLabel", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 变量已连接 的本地化字符串。
+ ///
+ public static string RaySource_VariablesConnected {
+ get {
+ return ResourceManager.GetString("RaySource_VariablesConnected", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 电压 (kV) 的本地化字符串。
+ ///
+ public static string RaySource_VoltageLabel {
+ get {
+ return ResourceManager.GetString("RaySource_VoltageLabel", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 是否确认执行暖机操作? 的本地化字符串。
+ ///
+ public static string RaySource_WarmUp_Confirm {
+ get {
+ return ResourceManager.GetString("RaySource_WarmUp_Confirm", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 正在执行暖机操作,请稍候... 的本地化字符串。
+ ///
+ public static string RaySource_WarmUp_Message {
+ get {
+ return ResourceManager.GetString("RaySource_WarmUp_Message", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 暖机设置 的本地化字符串。
+ ///
+ public static string RaySource_WarmUp_Title {
+ get {
+ return ResourceManager.GetString("RaySource_WarmUp_Title", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 X射线源预热完成 的本地化字符串。
+ ///
+ public static string RaySource_WarmupComplete {
+ get {
+ return ResourceManager.GetString("RaySource_WarmupComplete", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 X射线源预热中... 的本地化字符串。
+ ///
+ public static string RaySource_WarmupInProgress {
+ get {
+ return ResourceManager.GetString("RaySource_WarmupInProgress", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 暖机: 的本地化字符串。
+ ///
+ public static string RaySource_WarmUpLabel {
+ get {
+ return ResourceManager.GetString("RaySource_WarmUpLabel", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 暖机设置 的本地化字符串。
+ ///
+ public static string RaySource_WarmUpSettingButton {
+ get {
+ return ResourceManager.GetString("RaySource_WarmUpSettingButton", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 温度过高警告 的本地化字符串。
+ ///
+ public static string RaySource_Warning_HighTemperature {
+ get {
+ return ResourceManager.GetString("RaySource_Warning_HighTemperature", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 真空度过低警告 的本地化字符串。
+ ///
+ public static string RaySource_Warning_LowVacuum {
+ get {
+ return ResourceManager.GetString("RaySource_Warning_LowVacuum", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 看门狗: 的本地化字符串。
+ ///
+ public static string RaySource_WatchdogLabel {
+ get {
+ return ResourceManager.GetString("RaySource_WatchdogLabel", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 关闭 的本地化字符串。
+ ///
+ public static string RaySource_XRayOff {
+ get {
+ return ResourceManager.GetString("RaySource_XRayOff", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 开启 的本地化字符串。
+ ///
+ public static string RaySource_XRayOn {
+ get {
+ return ResourceManager.GetString("RaySource_XRayOn", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 射线状态: 的本地化字符串。
+ ///
+ public static string RaySource_XRayOnLabel {
+ get {
+ return ResourceManager.GetString("RaySource_XRayOnLabel", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 语言设置 的本地化字符串。
+ ///
+ public static string Settings_Language {
+ get {
+ return ResourceManager.GetString("Settings_Language", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 选择您偏好的显示语言 的本地化字符串。
+ ///
+ public static string Settings_Language_Description {
+ get {
+ return ResourceManager.GetString("Settings_Language_Description", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 语言切换将在下次启动应用程序时生效。 的本地化字符串。
+ ///
+ public static string Settings_Language_RestartNotice {
+ get {
+ return ResourceManager.GetString("Settings_Language_RestartNotice", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 语言设置已保存,请重启应用程序以应用新语言。 的本地化字符串。
+ ///
+ public static string Settings_Language_SavedRestartRequired {
+ get {
+ return ResourceManager.GetString("Settings_Language_SavedRestartRequired", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 语言切换失败:{0} 的本地化字符串。
+ ///
+ public static string Settings_Language_SwitchFailed {
+ get {
+ return ResourceManager.GetString("Settings_Language_SwitchFailed", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 已连接 的本地化字符串。
+ ///
+ public static string Status_Connected {
+ get {
+ return ResourceManager.GetString("Status_Connected", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 已断开 的本地化字符串。
+ ///
+ public static string Status_Disconnected {
+ get {
+ return ResourceManager.GetString("Status_Disconnected", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 错误 的本地化字符串。
+ ///
+ public static string Status_Error {
+ get {
+ return ResourceManager.GetString("Status_Error", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 失败 的本地化字符串。
+ ///
+ public static string Status_Failed {
+ get {
+ return ResourceManager.GetString("Status_Failed", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 加载中... 的本地化字符串。
+ ///
+ public static string Status_Loading {
+ get {
+ return ResourceManager.GetString("Status_Loading", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 处理中... 的本地化字符串。
+ ///
+ public static string Status_Processing {
+ get {
+ return ResourceManager.GetString("Status_Processing", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 就绪 的本地化字符串。
+ ///
+ public static string Status_Ready {
+ get {
+ return ResourceManager.GetString("Status_Ready", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 保存中... 的本地化字符串。
+ ///
+ public static string Status_Saving {
+ get {
+ return ResourceManager.GetString("Status_Saving", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 成功 的本地化字符串。
+ ///
+ public static string Status_Success {
+ get {
+ return ResourceManager.GetString("Status_Success", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 警告 的本地化字符串。
+ ///
+ public static string Status_Warning {
+ get {
+ return ResourceManager.GetString("Status_Warning", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 欢迎使用 XplorePlane X射线检测系统 的本地化字符串。
+ ///
+ public static string Welcome_Message {
+ get {
+ return ResourceManager.GetString("Welcome_Message", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/XP.Common/Resources/Resources.en-US.resx b/XP.Common/Resources/Resources.en-US.resx
new file mode 100644
index 0000000..8a59f8e
--- /dev/null
+++ b/XP.Common/Resources/Resources.en-US.resx
@@ -0,0 +1,378 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ XplorePlane X-Ray Inspection System
+
+
+ File
+
+
+ Settings
+
+
+ OK
+
+
+ Cancel
+
+
+ Simplified Chinese
+
+
+ Traditional Chinese
+
+
+ English
+
+
+ Language Settings
+
+
+ Select your preferred display language
+
+
+ Language changes will take effect on the next application startup.
+
+
+ Apply
+
+
+ Close
+
+
+ Save
+
+
+ Delete
+
+
+ Edit
+
+
+ Add
+
+
+ Refresh
+
+
+ Search
+
+
+ Reset
+
+
+ Ready
+
+
+ Loading...
+
+
+ Saving...
+
+
+ Processing...
+
+
+ Success
+
+
+ Failed
+
+
+ Error
+
+
+ Warning
+
+
+ Connected
+
+
+ Disconnected
+
+
+ Welcome to XplorePlane X-Ray Inspection System
+
+
+ Operation completed successfully
+
+
+ Operation failed
+
+
+ Are you sure you want to delete?
+
+
+ There are unsaved changes. Do you want to save?
+
+
+ Invalid input. Please check and try again
+
+
+ Connection lost
+
+
+ Language setting saved. Please restart the application to apply the new language.
+
+
+ View
+
+
+ Tools
+
+
+ Help
+
+
+ Exit
+
+
+ About
+
+
+ Confirmation
+
+
+ Information
+
+
+ Warning
+
+
+ Error
+
+
+ Notice
+
+
+ Language setting saved. Please restart the application to apply the new language.
+
+
+ Failed to switch language: {0}
+
+
+ Scan Mode:
+ Scan - Scan mode label
+
+
+ Frame Merge:
+ Scan - Frame merge label
+
+
+ Acquisition Count:
+ Scan - Acquisition count label
+
+
+ Rotation Angle:
+ Scan - Rotation angle label
+
+
+ Progress:
+ Scan - Acquisition progress label
+
+
+ Start
+ Scan - Start acquisition button
+
+
+ Stop
+ Scan - Stop acquisition button
+
+
+ Total {0} logs
+ LogViewer - Status bar total count
+
+
+ Total {0} logs, {1} after filtering
+ LogViewer - Status bar filtered count
+
+
+ Real-Time Log Viewer
+ LogViewer - Window title
+
+
+ Auto Scroll
+ LogViewer - Auto-scroll button
+
+
+ Clear Log
+ LogViewer - Clear log button
+
+
+ Filter:
+ LogViewer - Filter label
+
+
+ Enter keyword to filter logs...
+ LogViewer - Filter watermark
+
+
+ Level Filter:
+ LogViewer - Level filter label
+
+
+ Time
+ LogViewer - Time column header
+
+
+ Level
+ LogViewer - Level column header
+
+
+ Source
+ LogViewer - Source column header
+
+
+ Message
+ LogViewer - Message column header
+
+
+ Max Lines:
+ LogViewer - Max lines label
+
+
+ PDF Viewer
+ PdfViewer - Window title
+
+
+ PDF Viewer - {0}
+ PdfViewer - Window title with file name
+
+
+ PDF loaded: {0} ({1} pages)
+ PdfViewer - Load success log
+
+
+ PDF file load failed
+ PdfViewer - Load failed log
+
+
+ Print job submitted: {0} → {1}
+ PdfViewer - Print success log
+
+
+ Print failed
+ PdfViewer - Print failed log
+
+
+ Printer not found: {0}
+ PdfViewer - Printer not found
+
+
\ No newline at end of file
diff --git a/XP.Common/Resources/Resources.resx b/XP.Common/Resources/Resources.resx
new file mode 100644
index 0000000..d2680b2
--- /dev/null
+++ b/XP.Common/Resources/Resources.resx
@@ -0,0 +1,378 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ XplorePlane X射线检测系统
+
+
+ 文件
+
+
+ 设置
+
+
+ 确定
+
+
+ 取消
+
+
+ 简体中文
+
+
+ 繁體中文
+
+
+ English
+
+
+ 语言设置
+
+
+ 选择您偏好的显示语言
+
+
+ 语言切换将在下次启动应用程序时生效。
+
+
+ 应用
+
+
+ 关闭
+
+
+ 保存
+
+
+ 删除
+
+
+ 编辑
+
+
+ 添加
+
+
+ 刷新
+
+
+ 搜索
+
+
+ 重置
+
+
+ 就绪
+
+
+ 加载中...
+
+
+ 保存中...
+
+
+ 处理中...
+
+
+ 成功
+
+
+ 失败
+
+
+ 错误
+
+
+ 警告
+
+
+ 已连接
+
+
+ 已断开
+
+
+ 欢迎使用 XplorePlane X射线检测系统
+
+
+ 操作成功完成
+
+
+ 操作失败
+
+
+ 确定要删除吗?
+
+
+ 有未保存的更改,是否保存?
+
+
+ 输入无效,请检查后重试
+
+
+ 连接已断开
+
+
+ 语言设置已保存,请重启应用程序以应用新语言。
+
+
+ 视图
+
+
+ 工具
+
+
+ 帮助
+
+
+ 退出
+
+
+ 关于
+
+
+ 确认
+
+
+ 信息
+
+
+ 警告
+
+
+ 错误
+
+
+ 提示
+
+
+ 语言设置已保存,请重启应用程序以应用新语言。
+
+
+ 语言切换失败:{0}
+
+
+ 采集模式:
+ Scan - 采集模式标签 | Scan mode label
+
+
+ 帧合并:
+ Scan - 帧合并标签 | Frame merge label
+
+
+ 采集张数:
+ Scan - 采集张数标签 | Acquisition count label
+
+
+ 旋转角度:
+ Scan - 旋转角度标签 | Rotation angle label
+
+
+ 采集进度:
+ Scan - 采集进度标签 | Acquisition progress label
+
+
+ 开始采集
+ Scan - 开始采集按钮 | Start acquisition button
+
+
+ 停止采集
+ Scan - 停止采集按钮 | Stop acquisition button
+
+
+ 共 {0} 条日志
+ LogViewer - 状态栏总数 | Status bar total count
+
+
+ 共 {0} 条日志,过滤后 {1} 条
+ LogViewer - 状态栏过滤后 | Status bar filtered count
+
+
+ 实时日志查看器
+ LogViewer - 窗口标题 | Window title
+
+
+ 自动滚动
+ LogViewer - 自动滚动按钮 | Auto-scroll button
+
+
+ 清空日志
+ LogViewer - 清空日志按钮 | Clear log button
+
+
+ 过滤:
+ LogViewer - 过滤标签 | Filter label
+
+
+ 输入关键词过滤日志...
+ LogViewer - 过滤水印 | Filter watermark
+
+
+ 级别筛选:
+ LogViewer - 级别筛选标签 | Level filter label
+
+
+ 时间
+ LogViewer - 时间列头 | Time column header
+
+
+ 级别
+ LogViewer - 级别列头 | Level column header
+
+
+ 来源
+ LogViewer - 来源列头 | Source column header
+
+
+ 消息
+ LogViewer - 消息列头 | Message column header
+
+
+ 最大行数:
+ LogViewer - 最大行数标签 | Max lines label
+
+
+ PDF 阅读器
+ PdfViewer - 窗口标题 | Window title
+
+
+ PDF 阅读器 - {0}
+ PdfViewer - 带文件名的窗口标题 | Window title with file name
+
+
+ PDF 文件加载成功:{0}({1} 页)
+ PdfViewer - 加载成功日志 | Load success log
+
+
+ PDF 文件加载失败
+ PdfViewer - 加载失败日志 | Load failed log
+
+
+ 打印任务已提交:{0} → {1}
+ PdfViewer - 打印成功日志 | Print success log
+
+
+ 打印失败
+ PdfViewer - 打印失败日志 | Print failed log
+
+
+ 打印机未找到:{0}
+ PdfViewer - 打印机未找到 | Printer not found
+
+
\ No newline at end of file
diff --git a/XP.Common/Resources/Resources.zh-CN.resx b/XP.Common/Resources/Resources.zh-CN.resx
new file mode 100644
index 0000000..d2680b2
--- /dev/null
+++ b/XP.Common/Resources/Resources.zh-CN.resx
@@ -0,0 +1,378 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ XplorePlane X射线检测系统
+
+
+ 文件
+
+
+ 设置
+
+
+ 确定
+
+
+ 取消
+
+
+ 简体中文
+
+
+ 繁體中文
+
+
+ English
+
+
+ 语言设置
+
+
+ 选择您偏好的显示语言
+
+
+ 语言切换将在下次启动应用程序时生效。
+
+
+ 应用
+
+
+ 关闭
+
+
+ 保存
+
+
+ 删除
+
+
+ 编辑
+
+
+ 添加
+
+
+ 刷新
+
+
+ 搜索
+
+
+ 重置
+
+
+ 就绪
+
+
+ 加载中...
+
+
+ 保存中...
+
+
+ 处理中...
+
+
+ 成功
+
+
+ 失败
+
+
+ 错误
+
+
+ 警告
+
+
+ 已连接
+
+
+ 已断开
+
+
+ 欢迎使用 XplorePlane X射线检测系统
+
+
+ 操作成功完成
+
+
+ 操作失败
+
+
+ 确定要删除吗?
+
+
+ 有未保存的更改,是否保存?
+
+
+ 输入无效,请检查后重试
+
+
+ 连接已断开
+
+
+ 语言设置已保存,请重启应用程序以应用新语言。
+
+
+ 视图
+
+
+ 工具
+
+
+ 帮助
+
+
+ 退出
+
+
+ 关于
+
+
+ 确认
+
+
+ 信息
+
+
+ 警告
+
+
+ 错误
+
+
+ 提示
+
+
+ 语言设置已保存,请重启应用程序以应用新语言。
+
+
+ 语言切换失败:{0}
+
+
+ 采集模式:
+ Scan - 采集模式标签 | Scan mode label
+
+
+ 帧合并:
+ Scan - 帧合并标签 | Frame merge label
+
+
+ 采集张数:
+ Scan - 采集张数标签 | Acquisition count label
+
+
+ 旋转角度:
+ Scan - 旋转角度标签 | Rotation angle label
+
+
+ 采集进度:
+ Scan - 采集进度标签 | Acquisition progress label
+
+
+ 开始采集
+ Scan - 开始采集按钮 | Start acquisition button
+
+
+ 停止采集
+ Scan - 停止采集按钮 | Stop acquisition button
+
+
+ 共 {0} 条日志
+ LogViewer - 状态栏总数 | Status bar total count
+
+
+ 共 {0} 条日志,过滤后 {1} 条
+ LogViewer - 状态栏过滤后 | Status bar filtered count
+
+
+ 实时日志查看器
+ LogViewer - 窗口标题 | Window title
+
+
+ 自动滚动
+ LogViewer - 自动滚动按钮 | Auto-scroll button
+
+
+ 清空日志
+ LogViewer - 清空日志按钮 | Clear log button
+
+
+ 过滤:
+ LogViewer - 过滤标签 | Filter label
+
+
+ 输入关键词过滤日志...
+ LogViewer - 过滤水印 | Filter watermark
+
+
+ 级别筛选:
+ LogViewer - 级别筛选标签 | Level filter label
+
+
+ 时间
+ LogViewer - 时间列头 | Time column header
+
+
+ 级别
+ LogViewer - 级别列头 | Level column header
+
+
+ 来源
+ LogViewer - 来源列头 | Source column header
+
+
+ 消息
+ LogViewer - 消息列头 | Message column header
+
+
+ 最大行数:
+ LogViewer - 最大行数标签 | Max lines label
+
+
+ PDF 阅读器
+ PdfViewer - 窗口标题 | Window title
+
+
+ PDF 阅读器 - {0}
+ PdfViewer - 带文件名的窗口标题 | Window title with file name
+
+
+ PDF 文件加载成功:{0}({1} 页)
+ PdfViewer - 加载成功日志 | Load success log
+
+
+ PDF 文件加载失败
+ PdfViewer - 加载失败日志 | Load failed log
+
+
+ 打印任务已提交:{0} → {1}
+ PdfViewer - 打印成功日志 | Print success log
+
+
+ 打印失败
+ PdfViewer - 打印失败日志 | Print failed log
+
+
+ 打印机未找到:{0}
+ PdfViewer - 打印机未找到 | Printer not found
+
+
\ No newline at end of file
diff --git a/XP.Common/Resources/Resources.zh-TW.resx b/XP.Common/Resources/Resources.zh-TW.resx
new file mode 100644
index 0000000..ce4089a
--- /dev/null
+++ b/XP.Common/Resources/Resources.zh-TW.resx
@@ -0,0 +1,378 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ XplorePlane X射線檢測系統
+
+
+ 檔案
+
+
+ 設定
+
+
+ 確定
+
+
+ 取消
+
+
+ 簡體中文
+
+
+ 繁體中文
+
+
+ English
+
+
+ 語言設定
+
+
+ 選擇您偏好的顯示語言
+
+
+ 語言切換將在下次啟動應用程式時生效。
+
+
+ 套用
+
+
+ 關閉
+
+
+ 儲存
+
+
+ 刪除
+
+
+ 編輯
+
+
+ 新增
+
+
+ 重新整理
+
+
+ 搜尋
+
+
+ 重設
+
+
+ 就緒
+
+
+ 載入中...
+
+
+ 儲存中...
+
+
+ 處理中...
+
+
+ 成功
+
+
+ 失敗
+
+
+ 錯誤
+
+
+ 警告
+
+
+ 已連線
+
+
+ 已中斷連線
+
+
+ 歡迎使用 XplorePlane X射線檢測系統
+
+
+ 操作成功完成
+
+
+ 操作失敗
+
+
+ 確定要刪除嗎?
+
+
+ 有未儲存的變更,是否儲存?
+
+
+ 輸入無效,請檢查後重試
+
+
+ 連線已中斷
+
+
+ 語言設定已儲存,請重新啟動應用程式以套用新語言。
+
+
+ 檢視
+
+
+ 工具
+
+
+ 說明
+
+
+ 結束
+
+
+ 關於
+
+
+ 確認
+
+
+ 資訊
+
+
+ 警告
+
+
+ 錯誤
+
+
+ 提示
+
+
+ 語言設定已儲存,請重新啟動應用程式以套用新語言。
+
+
+ 語言切換失敗:{0}
+
+
+ 採集模式:
+ Scan - 採集模式標籤 | Scan mode label
+
+
+ 幀合併:
+ Scan - 幀合併標籤 | Frame merge label
+
+
+ 採集張數:
+ Scan - 採集張數標籤 | Acquisition count label
+
+
+ 旋轉角度:
+ Scan - 旋轉角度標籤 | Rotation angle label
+
+
+ 採集進度:
+ Scan - 採集進度標籤 | Acquisition progress label
+
+
+ 開始採集
+ Scan - 開始採集按鈕 | Start acquisition button
+
+
+ 停止採集
+ Scan - 停止採集按鈕 | Stop acquisition button
+
+
+ 共 {0} 筆日誌
+ LogViewer - 狀態列總數 | Status bar total count
+
+
+ 共 {0} 筆日誌,篩選後 {1} 筆
+ LogViewer - 狀態列篩選後 | Status bar filtered count
+
+
+ 即時日誌檢視器
+ LogViewer - 視窗標題 | Window title
+
+
+ 自動捲動
+ LogViewer - 自動捲動按鈕 | Auto-scroll button
+
+
+ 清空日誌
+ LogViewer - 清空日誌按鈕 | Clear log button
+
+
+ 篩選:
+ LogViewer - 篩選標籤 | Filter label
+
+
+ 輸入關鍵詞篩選日誌...
+ LogViewer - 篩選浮水印 | Filter watermark
+
+
+ 級別篩選:
+ LogViewer - 級別篩選標籤 | Level filter label
+
+
+ 時間
+ LogViewer - 時間欄頭 | Time column header
+
+
+ 級別
+ LogViewer - 級別欄頭 | Level column header
+
+
+ 來源
+ LogViewer - 來源欄頭 | Source column header
+
+
+ 訊息
+ LogViewer - 訊息欄頭 | Message column header
+
+
+ 最大行數:
+ LogViewer - 最大行數標籤 | Max lines label
+
+
+ PDF 閱讀器
+ PdfViewer - 視窗標題 | Window title
+
+
+ PDF 閱讀器 - {0}
+ PdfViewer - 帶檔案名稱的視窗標題 | Window title with file name
+
+
+ PDF 檔案載入成功:{0}({1} 頁)
+ PdfViewer - 載入成功日誌 | Load success log
+
+
+ PDF 檔案載入失敗
+ PdfViewer - 載入失敗日誌 | Load failed log
+
+
+ 列印任務已提交:{0} → {1}
+ PdfViewer - 列印成功日誌 | Print success log
+
+
+ 列印失敗
+ PdfViewer - 列印失敗日誌 | Print failed log
+
+
+ 印表機未找到:{0}
+ PdfViewer - 印表機未找到 | Printer not found
+
+
\ No newline at end of file
diff --git a/XP.Common/XP.Common.csproj b/XP.Common/XP.Common.csproj
new file mode 100644
index 0000000..6a45ec0
--- /dev/null
+++ b/XP.Common/XP.Common.csproj
@@ -0,0 +1,67 @@
+
+
+ net8.0-windows7.0
+ true
+
+ 1.0.0
+
+ 1.4.16.1
+
+ 1.4.16.1
+
+ Copyright © 2026 Hexagon. All rights reserved.
+
+ Hexagon Manufacturing Intelligence (Qingdao) Co., Ltd.
+
+ XplorePlane XP.Common Infrastructure
+
+ XP.Common Core Library
+ true
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PublicResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+ Resources.resx
+
+
+ Resources.resx
+
+
+ Resources.resx
+
+
+
+
+ True
+ True
+ Resources.resx
+
+
+
\ No newline at end of file
diff --git a/XP.Hardware.Detector/Abstractions/AreaDetectorBase.cs b/XP.Hardware.Detector/Abstractions/AreaDetectorBase.cs
new file mode 100644
index 0000000..8964a33
--- /dev/null
+++ b/XP.Hardware.Detector/Abstractions/AreaDetectorBase.cs
@@ -0,0 +1,533 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Prism.Events;
+using XP.Hardware.Detector.Abstractions.Enums;
+using XP.Hardware.Detector.Abstractions.Events;
+
+namespace XP.Hardware.Detector.Abstractions
+{
+ ///
+ /// 面阵探测器抽象基类 | Area detector abstract base class
+ /// 封装通用逻辑:参数校验、状态更新、事件发布
+ /// 使用模板方法模式,子类实现具体的硬件操作
+ ///
+ public abstract class AreaDetectorBase : IAreaDetector
+ {
+ protected readonly IEventAggregator _eventAggregator;
+ protected readonly object _statusLock = new object();
+ protected DetectorStatus _status = DetectorStatus.Uninitialized;
+ protected bool _disposed = false;
+
+ ///
+ /// 探测器状态 | Detector status
+ ///
+ public DetectorStatus Status
+ {
+ get
+ {
+ lock (_statusLock)
+ {
+ return _status;
+ }
+ }
+ }
+
+ ///
+ /// 探测器类型 | Detector type
+ ///
+ public abstract DetectorType Type { get; }
+
+ ///
+ /// 构造函数 | Constructor
+ ///
+ /// 事件聚合器 | Event aggregator
+ protected AreaDetectorBase(IEventAggregator eventAggregator)
+ {
+ _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
+ }
+
+ ///
+ /// 初始化探测器 | Initialize detector
+ ///
+ public async Task InitializeAsync(CancellationToken cancellationToken = default)
+ {
+ // 参数校验 | Parameter validation
+ if (Status != DetectorStatus.Uninitialized)
+ {
+ return DetectorResult.Failure("探测器已初始化 | Detector already initialized");
+ }
+
+ try
+ {
+ // 更新状态 | Update status
+ UpdateStatus(DetectorStatus.Initializing);
+
+ // 调用子类实现 | Call derived class implementation
+ var result = await InitializeInternalAsync(cancellationToken);
+
+ if (result.IsSuccess)
+ {
+ UpdateStatus(DetectorStatus.Ready);
+ }
+ else
+ {
+ UpdateStatus(DetectorStatus.Error);
+ PublishError(result);
+ }
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ UpdateStatus(DetectorStatus.Error);
+ var errorResult = DetectorResult.Failure($"初始化异常 | Initialization exception: {ex.Message}", ex);
+ PublishError(errorResult);
+ return errorResult;
+ }
+ }
+
+ ///
+ /// 启动连续采集 | Start continuous acquisition
+ ///
+ public async Task StartAcquisitionAsync(CancellationToken cancellationToken = default)
+ {
+ // 参数校验 | Parameter validation
+ if (Status != DetectorStatus.Ready)
+ {
+ return DetectorResult.Failure($"探测器状态不正确,当前状态:{Status} | Detector status incorrect, current status: {Status}");
+ }
+
+ try
+ {
+ // 更新状态 | Update status
+ UpdateStatus(DetectorStatus.Acquiring);
+
+ // 调用子类实现 | Call derived class implementation
+ var result = await StartAcquisitionInternalAsync(cancellationToken);
+
+ if (!result.IsSuccess)
+ {
+ UpdateStatus(DetectorStatus.Ready);
+ PublishError(result);
+ }
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ UpdateStatus(DetectorStatus.Error);
+ var errorResult = DetectorResult.Failure($"启动采集异常 | Start acquisition exception: {ex.Message}", ex);
+ PublishError(errorResult);
+ return errorResult;
+ }
+ }
+
+ ///
+ /// 停止采集 | Stop acquisition
+ ///
+ public async Task StopAcquisitionAsync(CancellationToken cancellationToken = default)
+ {
+ // 参数校验 | Parameter validation
+ // 未初始化时不允许停止采集操作 | Stop acquisition not allowed when uninitialized
+ if (Status == DetectorStatus.Uninitialized)
+ {
+ return DetectorResult.Failure("探测器未初始化,无法执行停止采集操作 | Detector not initialized, cannot stop acquisition");
+ }
+
+ try
+ {
+ // 调用子类实现 | Call derived class implementation
+ var result = await StopAcquisitionInternalAsync(cancellationToken);
+
+ if (result.IsSuccess)
+ {
+ UpdateStatus(DetectorStatus.Ready);
+ }
+ else
+ {
+ PublishError(result);
+ }
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ UpdateStatus(DetectorStatus.Error);
+ var errorResult = DetectorResult.Failure($"停止采集异常 | Stop acquisition exception: {ex.Message}", ex);
+ PublishError(errorResult);
+ return errorResult;
+ }
+ }
+
+ ///
+ /// 单帧采集 | Single frame acquisition
+ ///
+ public async Task AcquireSingleFrameAsync(CancellationToken cancellationToken = default)
+ {
+ // 参数校验 | Parameter validation
+ if (Status != DetectorStatus.Ready)
+ {
+ return DetectorResult.Failure($"探测器状态不正确,当前状态:{Status} | Detector status incorrect, current status: {Status}");
+ }
+
+ try
+ {
+ // 更新状态 | Update status
+ UpdateStatus(DetectorStatus.Acquiring);
+
+ // 调用子类实现 | Call derived class implementation
+ var result = await AcquireSingleFrameInternalAsync(cancellationToken);
+
+ // 恢复状态 | Restore status
+ UpdateStatus(DetectorStatus.Ready);
+
+ if (!result.IsSuccess)
+ {
+ PublishError(result);
+ }
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ UpdateStatus(DetectorStatus.Error);
+ var errorResult = DetectorResult.Failure($"单帧采集异常 | Single frame acquisition exception: {ex.Message}", ex);
+ PublishError(errorResult);
+ return errorResult;
+ }
+ }
+
+ ///
+ /// 暗场校正 | Dark field correction
+ ///
+ public async Task DarkCorrectionAsync(int frameCount, CancellationToken cancellationToken = default)
+ {
+ // 参数校验 | Parameter validation
+ if (Status != DetectorStatus.Ready)
+ {
+ return DetectorResult.Failure($"探测器状态不正确,当前状态:{Status} | Detector status incorrect, current status: {Status}");
+ }
+
+ if (frameCount <= 0)
+ {
+ return DetectorResult.Failure("帧数必须大于 0 | Frame count must be greater than 0");
+ }
+
+ try
+ {
+ // 更新状态 | Update status
+ UpdateStatus(DetectorStatus.Correcting);
+
+ // 调用子类实现 | Call derived class implementation
+ var result = await DarkCorrectionInternalAsync(frameCount, cancellationToken);
+
+ // 恢复状态 | Restore status
+ UpdateStatus(DetectorStatus.Ready);
+
+ if (result.IsSuccess)
+ {
+ PublishCorrectionCompleted(CorrectionType.Dark, result);
+ }
+ else
+ {
+ PublishError(result);
+ }
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ UpdateStatus(DetectorStatus.Error);
+ var errorResult = DetectorResult.Failure($"暗场校正异常 | Dark correction exception: {ex.Message}", ex);
+ PublishError(errorResult);
+ return errorResult;
+ }
+ }
+
+ ///
+ /// 增益校正 | Gain correction
+ ///
+ public async Task GainCorrectionAsync(int frameCount, CancellationToken cancellationToken = default)
+ {
+ // 参数校验 | Parameter validation
+ if (Status != DetectorStatus.Ready)
+ {
+ return DetectorResult.Failure($"探测器状态不正确,当前状态:{Status} | Detector status incorrect, current status: {Status}");
+ }
+
+ if (frameCount <= 0)
+ {
+ return DetectorResult.Failure("帧数必须大于 0 | Frame count must be greater than 0");
+ }
+
+ try
+ {
+ // 更新状态 | Update status
+ UpdateStatus(DetectorStatus.Correcting);
+
+ // 调用子类实现 | Call derived class implementation
+ var result = await GainCorrectionInternalAsync(frameCount, cancellationToken);
+
+ // 恢复状态 | Restore status
+ UpdateStatus(DetectorStatus.Ready);
+
+ if (result.IsSuccess)
+ {
+ PublishCorrectionCompleted(CorrectionType.Gain, result);
+ }
+ else
+ {
+ PublishError(result);
+ }
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ UpdateStatus(DetectorStatus.Error);
+ var errorResult = DetectorResult.Failure($"增益校正异常 | Gain correction exception: {ex.Message}", ex);
+ PublishError(errorResult);
+ return errorResult;
+ }
+ }
+
+ ///
+ /// 自动校正 | Auto correction
+ ///
+ public async Task AutoCorrectionAsync(int frameCount, CancellationToken cancellationToken = default)
+ {
+ // 参数校验 | Parameter validation
+ if (Status != DetectorStatus.Ready)
+ {
+ return DetectorResult.Failure($"探测器状态不正确,当前状态:{Status} | Detector status incorrect, current status: {Status}");
+ }
+
+ if (frameCount <= 0)
+ {
+ return DetectorResult.Failure("帧数必须大于 0 | Frame count must be greater than 0");
+ }
+
+ try
+ {
+ // 更新状态 | Update status
+ UpdateStatus(DetectorStatus.Correcting);
+
+ // 调用子类实现 | Call derived class implementation
+ var result = await AutoCorrectionInternalAsync(frameCount, cancellationToken);
+
+ // 恢复状态 | Restore status
+ UpdateStatus(DetectorStatus.Ready);
+
+ if (result.IsSuccess)
+ {
+ PublishCorrectionCompleted(CorrectionType.Auto, result);
+ }
+ else
+ {
+ PublishError(result);
+ }
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ UpdateStatus(DetectorStatus.Error);
+ var errorResult = DetectorResult.Failure($"自动校正异常 | Auto correction exception: {ex.Message}", ex);
+ PublishError(errorResult);
+ return errorResult;
+ }
+ }
+
+ ///
+ /// 坏像素校正 | Bad pixel correction
+ ///
+ public async Task BadPixelCorrectionAsync(CancellationToken cancellationToken = default)
+ {
+ // 参数校验 | Parameter validation
+ if (Status != DetectorStatus.Ready)
+ {
+ return DetectorResult.Failure($"探测器状态不正确,当前状态:{Status} | Detector status incorrect, current status: {Status}");
+ }
+
+ try
+ {
+ // 更新状态 | Update status
+ UpdateStatus(DetectorStatus.Correcting);
+
+ // 调用子类实现 | Call derived class implementation
+ var result = await BadPixelCorrectionInternalAsync(cancellationToken);
+
+ // 恢复状态 | Restore status
+ UpdateStatus(DetectorStatus.Ready);
+
+ if (result.IsSuccess)
+ {
+ PublishCorrectionCompleted(CorrectionType.BadPixel, result);
+ }
+ else
+ {
+ PublishError(result);
+ }
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ UpdateStatus(DetectorStatus.Error);
+ var errorResult = DetectorResult.Failure($"坏像素校正异常 | Bad pixel correction exception: {ex.Message}", ex);
+ PublishError(errorResult);
+ return errorResult;
+ }
+ }
+
+ ///
+ /// 获取探测器信息 | Get detector information
+ ///
+ public abstract DetectorInfo GetInfo();
+
+ // 模板方法,由子类实现 | Template methods, implemented by derived classes
+ protected abstract Task InitializeInternalAsync(CancellationToken cancellationToken);
+ protected abstract Task StartAcquisitionInternalAsync(CancellationToken cancellationToken);
+ protected abstract Task StopAcquisitionInternalAsync(CancellationToken cancellationToken);
+ protected abstract Task AcquireSingleFrameInternalAsync(CancellationToken cancellationToken);
+ protected abstract Task DarkCorrectionInternalAsync(int frameCount, CancellationToken cancellationToken);
+ protected abstract Task GainCorrectionInternalAsync(int frameCount, CancellationToken cancellationToken);
+ protected abstract Task AutoCorrectionInternalAsync(int frameCount, CancellationToken cancellationToken);
+ protected abstract Task BadPixelCorrectionInternalAsync(CancellationToken cancellationToken);
+
+ ///
+ /// 更新状态并发布事件 | Update status and publish event
+ ///
+ /// 新状态 | New status
+ ///
+ /// 更新状态并发布事件 | Update status and publish event
+ /// 验证状态转换的合法性 | Validate state transition legality
+ ///
+ /// 新状态 | New status
+ protected void UpdateStatus(DetectorStatus newStatus)
+ {
+ lock (_statusLock)
+ {
+ // 验证状态转换的合法性 | Validate state transition legality
+ if (!IsValidStateTransition(_status, newStatus))
+ {
+ var errorMsg = $"非法的状态转换:{_status} -> {newStatus} | Invalid state transition: {_status} -> {newStatus}";
+ PublishError(DetectorResult.Failure(errorMsg));
+ return;
+ }
+
+ if (_status != newStatus)
+ {
+ var oldStatus = _status;
+ _status = newStatus;
+ _eventAggregator.GetEvent().Publish(newStatus);
+
+ // 记录状态转换日志(如果需要)| Log state transition (if needed)
+ System.Diagnostics.Debug.WriteLine($"状态转换 | State transition: {oldStatus} -> {newStatus}");
+ }
+ }
+ }
+
+ ///
+ /// 验证状态转换是否合法 | Validate if state transition is legal
+ ///
+ /// 当前状态 | Current status
+ /// 新状态 | New status
+ /// 是否合法 | Whether it's legal
+ private bool IsValidStateTransition(DetectorStatus currentStatus, DetectorStatus newStatus)
+ {
+ // 相同状态总是允许的 | Same state is always allowed
+ if (currentStatus == newStatus)
+ {
+ return true;
+ }
+
+ // 定义合法的状态转换规则 | Define legal state transition rules
+ return (currentStatus, newStatus) switch
+ {
+ // 从未初始化状态只能转到初始化中 | From Uninitialized can only go to Initializing
+ (DetectorStatus.Uninitialized, DetectorStatus.Initializing) => true,
+
+ // 从初始化中可以转到就绪或错误 | From Initializing can go to Ready or Error
+ (DetectorStatus.Initializing, DetectorStatus.Ready) => true,
+ (DetectorStatus.Initializing, DetectorStatus.Error) => true,
+
+ // 从就绪状态可以转到采集中、校正中 | From Ready can go to Acquiring or Correcting
+ (DetectorStatus.Ready, DetectorStatus.Acquiring) => true,
+ (DetectorStatus.Ready, DetectorStatus.Correcting) => true,
+
+ // 从采集中可以转到就绪或错误 | From Acquiring can go to Ready or Error
+ (DetectorStatus.Acquiring, DetectorStatus.Ready) => true,
+ (DetectorStatus.Acquiring, DetectorStatus.Error) => true,
+
+ // 从校正中可以转到就绪或错误 | From Correcting can go to Ready or Error
+ (DetectorStatus.Correcting, DetectorStatus.Ready) => true,
+ (DetectorStatus.Correcting, DetectorStatus.Error) => true,
+
+ // 从错误状态可以转到初始化中(重新初始化)| From Error can go to Initializing (re-initialize)
+ (DetectorStatus.Error, DetectorStatus.Initializing) => true,
+
+ // 任何状态都可以转到错误状态 | Any state can go to Error
+ (_, DetectorStatus.Error) => true,
+
+ // 其他转换都是非法的 | All other transitions are illegal
+ _ => false
+ };
+ }
+
+
+ ///
+ /// 发布错误事件 | Publish error event
+ ///
+ /// 错误结果 | Error result
+ protected void PublishError(DetectorResult result)
+ {
+ _eventAggregator.GetEvent().Publish(result);
+ }
+
+ ///
+ /// 发布图像采集事件 | Publish image captured event
+ ///
+ /// 事件参数 | Event arguments
+ protected void PublishImageCaptured(ImageCapturedEventArgs args)
+ {
+ _eventAggregator.GetEvent().Publish(args);
+ }
+
+ ///
+ /// 发布校正完成事件 | Publish correction completed event
+ ///
+ /// 校正类型 | Correction type
+ /// 校正结果 | Correction result
+ protected void PublishCorrectionCompleted(CorrectionType type, DetectorResult result)
+ {
+ _eventAggregator.GetEvent().Publish(
+ new CorrectionCompletedEventArgs(type, result));
+ }
+
+ // IDisposable 实现 | IDisposable implementation
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// 释放资源 | Dispose resources
+ ///
+ /// 是否释放托管资源 | Whether to dispose managed resources
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (disposing)
+ {
+ // 释放托管资源 | Release managed resources
+ }
+ // 释放非托管资源 | Release unmanaged resources
+ _disposed = true;
+ }
+ }
+ }
+}
diff --git a/XP.Hardware.Detector/Abstractions/DetectorInfo.cs b/XP.Hardware.Detector/Abstractions/DetectorInfo.cs
new file mode 100644
index 0000000..f3f317e
--- /dev/null
+++ b/XP.Hardware.Detector/Abstractions/DetectorInfo.cs
@@ -0,0 +1,51 @@
+using XP.Hardware.Detector.Abstractions.Enums;
+
+namespace XP.Hardware.Detector.Abstractions
+{
+ ///
+ /// 探测器信息 | Detector information
+ /// 包含探测器的硬件参数和配置信息
+ ///
+ public class DetectorInfo
+ {
+ ///
+ /// 探测器类型 | Detector type
+ ///
+ public DetectorType Type { get; set; }
+
+ ///
+ /// 探测器型号 | Detector model
+ ///
+ public string Model { get; set; }
+
+ ///
+ /// 序列号 | Serial number
+ ///
+ public string SerialNumber { get; set; }
+
+ ///
+ /// 固件版本 | Firmware version
+ ///
+ public string FirmwareVersion { get; set; }
+
+ ///
+ /// 最大分辨率宽度 | Maximum resolution width
+ ///
+ public uint MaxWidth { get; set; }
+
+ ///
+ /// 最大分辨率高度 | Maximum resolution height
+ ///
+ public uint MaxHeight { get; set; }
+
+ ///
+ /// 像素尺寸(微米)| Pixel size (micrometers)
+ ///
+ public double PixelSize { get; set; }
+
+ ///
+ /// 位深度 | Bit depth
+ ///
+ public int BitDepth { get; set; }
+ }
+}
diff --git a/XP.Hardware.Detector/Abstractions/DetectorResult.cs b/XP.Hardware.Detector/Abstractions/DetectorResult.cs
new file mode 100644
index 0000000..6fd4cb2
--- /dev/null
+++ b/XP.Hardware.Detector/Abstractions/DetectorResult.cs
@@ -0,0 +1,102 @@
+using System;
+
+namespace XP.Hardware.Detector.Abstractions
+{
+ ///
+ /// 探测器操作结果封装 | Detector operation result wrapper
+ /// 统一封装成功/失败状态、数据和错误信息
+ ///
+ public class DetectorResult
+ {
+ ///
+ /// 操作是否成功 | Whether the operation succeeded
+ ///
+ public bool IsSuccess { get; }
+
+ ///
+ /// 错误消息 | Error message
+ ///
+ public string ErrorMessage { get; }
+
+ ///
+ /// 异常对象 | Exception object
+ ///
+ public Exception Exception { get; }
+
+ ///
+ /// 错误码 | Error code
+ ///
+ public int ErrorCode { get; }
+
+ protected DetectorResult(bool isSuccess, string errorMessage = null, Exception exception = null, int errorCode = 0)
+ {
+ IsSuccess = isSuccess;
+ ErrorMessage = errorMessage;
+ Exception = exception;
+ ErrorCode = errorCode;
+ }
+
+ ///
+ /// 创建成功结果 | Create success result
+ ///
+ /// 成功消息 | Success message
+ /// 成功结果 | Success result
+ public static DetectorResult Success(string message = null)
+ {
+ return new DetectorResult(true, message);
+ }
+
+ ///
+ /// 创建失败结果 | Create failure result
+ ///
+ /// 错误消息 | Error message
+ /// 异常对象 | Exception object
+ /// 错误码 | Error code
+ /// 失败结果 | Failure result
+ public static DetectorResult Failure(string errorMessage, Exception exception = null, int errorCode = -1)
+ {
+ return new DetectorResult(false, errorMessage, exception, errorCode);
+ }
+ }
+
+ ///
+ /// 带数据的探测器操作结果 | Detector operation result with data
+ ///
+ /// 数据类型 | Data type
+ public class DetectorResult : DetectorResult
+ {
+ ///
+ /// 结果数据 | Result data
+ ///
+ public T Data { get; }
+
+ private DetectorResult(bool isSuccess, T data, string errorMessage = null, Exception exception = null, int errorCode = 0)
+ : base(isSuccess, errorMessage, exception, errorCode)
+ {
+ Data = data;
+ }
+
+ ///
+ /// 创建成功结果 | Create success result
+ ///
+ /// 结果数据 | Result data
+ /// 成功消息 | Success message
+ /// 成功结果 | Success result
+ public static DetectorResult Success(T data, string message = null)
+ {
+ return new DetectorResult(true, data, message);
+ }
+
+ ///
+ /// 创建失败结果 | Create failure result
+ ///
+ /// 错误消息 | Error message
+ /// 异常对象 | Exception object
+ /// 错误码 | Error code
+ /// 失败结果 | Failure result
+ public new static DetectorResult Failure(string errorMessage, Exception exception = null, int errorCode = -1)
+ {
+ return new DetectorResult(false, default(T), errorMessage, exception, errorCode);
+ }
+ }
+}
diff --git a/XP.Hardware.Detector/Abstractions/Enums/AcquisitionMode.cs b/XP.Hardware.Detector/Abstractions/Enums/AcquisitionMode.cs
new file mode 100644
index 0000000..dab9e71
--- /dev/null
+++ b/XP.Hardware.Detector/Abstractions/Enums/AcquisitionMode.cs
@@ -0,0 +1,19 @@
+namespace XP.Hardware.Detector.Abstractions.Enums
+{
+ ///
+ /// 采集模式枚举 | Acquisition mode enumeration
+ /// 定义图像采集的工作模式
+ ///
+ public enum AcquisitionMode
+ {
+ ///
+ /// 连续采集模式 | Continuous acquisition mode
+ ///
+ Continuous = 0,
+
+ ///
+ /// 单帧采集模式 | Single frame acquisition mode
+ ///
+ SingleFrame = 1
+ }
+}
diff --git a/XP.Hardware.Detector/Abstractions/Enums/BinningMode.cs b/XP.Hardware.Detector/Abstractions/Enums/BinningMode.cs
new file mode 100644
index 0000000..64b34fa
--- /dev/null
+++ b/XP.Hardware.Detector/Abstractions/Enums/BinningMode.cs
@@ -0,0 +1,24 @@
+namespace XP.Hardware.Detector.Abstractions.Enums
+{
+ ///
+ /// Binning 模式枚举 | Binning mode enumeration
+ /// 定义像素合并模式,用于提高信噪比或采集速度
+ ///
+ public enum BinningMode
+ {
+ ///
+ /// 1x1 模式(无合并)| 1x1 mode (no binning)
+ ///
+ Bin1x1 = 0,
+
+ ///
+ /// 2x2 模式(2x2 像素合并)| 2x2 mode (2x2 pixel binning)
+ ///
+ Bin2x2 = 1,
+
+ ///
+ /// 4x4 模式(4x4 像素合并)| 4x4 mode (4x4 pixel binning)
+ ///
+ Bin4x4 = 2
+ }
+}
diff --git a/XP.Hardware.Detector/Abstractions/Enums/CorrectionType.cs b/XP.Hardware.Detector/Abstractions/Enums/CorrectionType.cs
new file mode 100644
index 0000000..fd39243
--- /dev/null
+++ b/XP.Hardware.Detector/Abstractions/Enums/CorrectionType.cs
@@ -0,0 +1,29 @@
+namespace XP.Hardware.Detector.Abstractions.Enums
+{
+ ///
+ /// 校正类型枚举 | Correction type enumeration
+ /// 定义探测器支持的校正类型
+ ///
+ public enum CorrectionType
+ {
+ ///
+ /// 暗场校正 | Dark field correction
+ ///
+ Dark = 0,
+
+ ///
+ /// 增益校正(亮场校正)| Gain correction (bright field correction)
+ ///
+ Gain = 1,
+
+ ///
+ /// 自动校正(暗场+增益+坏像素)| Auto correction (dark + gain + bad pixel)
+ ///
+ Auto = 2,
+
+ ///
+ /// 坏像素校正 | Bad pixel correction
+ ///
+ BadPixel = 3
+ }
+}
diff --git a/XP.Hardware.Detector/Abstractions/Enums/DetectorStatus.cs b/XP.Hardware.Detector/Abstractions/Enums/DetectorStatus.cs
new file mode 100644
index 0000000..69e2b9c
--- /dev/null
+++ b/XP.Hardware.Detector/Abstractions/Enums/DetectorStatus.cs
@@ -0,0 +1,39 @@
+namespace XP.Hardware.Detector.Abstractions.Enums
+{
+ ///
+ /// 探测器状态枚举 | Detector status enumeration
+ /// 定义探测器的运行状态
+ ///
+ public enum DetectorStatus
+ {
+ ///
+ /// 未初始化 | Uninitialized
+ ///
+ Uninitialized = 0,
+
+ ///
+ /// 初始化中 | Initializing
+ ///
+ Initializing = 1,
+
+ ///
+ /// 就绪 | Ready
+ ///
+ Ready = 2,
+
+ ///
+ /// 采集中 | Acquiring
+ ///
+ Acquiring = 3,
+
+ ///
+ /// 校正中 | Correcting
+ ///
+ Correcting = 4,
+
+ ///
+ /// 错误 | Error
+ ///
+ Error = 5
+ }
+}
diff --git a/XP.Hardware.Detector/Abstractions/Enums/DetectorType.cs b/XP.Hardware.Detector/Abstractions/Enums/DetectorType.cs
new file mode 100644
index 0000000..88b0704
--- /dev/null
+++ b/XP.Hardware.Detector/Abstractions/Enums/DetectorType.cs
@@ -0,0 +1,24 @@
+namespace XP.Hardware.Detector.Abstractions.Enums
+{
+ ///
+ /// 探测器类型枚举 | Detector type enumeration
+ /// 定义支持的探测器厂商类型
+ ///
+ public enum DetectorType
+ {
+ ///
+ /// Varex 探测器 | Varex detector
+ ///
+ Varex = 0,
+
+ ///
+ /// iRay 探测器 | iRay detector (预留)
+ ///
+ IRay = 1,
+
+ ///
+ /// Hamamatsu 探测器 | Hamamatsu detector (预留)
+ ///
+ Hamamatsu = 2
+ }
+}
diff --git a/XP.Hardware.Detector/Abstractions/Enums/GainMode.cs b/XP.Hardware.Detector/Abstractions/Enums/GainMode.cs
new file mode 100644
index 0000000..592b149
--- /dev/null
+++ b/XP.Hardware.Detector/Abstractions/Enums/GainMode.cs
@@ -0,0 +1,19 @@
+namespace XP.Hardware.Detector.Abstractions.Enums
+{
+ ///
+ /// 增益模式枚举 | Gain mode enumeration
+ /// 定义探测器的信号放大倍数
+ ///
+ public enum GainMode
+ {
+ ///
+ /// 低增益模式 | Low gain mode
+ ///
+ Low = 0,
+
+ ///
+ /// 高增益模式 | High gain mode
+ ///
+ High = 1
+ }
+}
diff --git a/XP.Hardware.Detector/Abstractions/Events/CorrectionCompletedEvent.cs b/XP.Hardware.Detector/Abstractions/Events/CorrectionCompletedEvent.cs
new file mode 100644
index 0000000..fbe09eb
--- /dev/null
+++ b/XP.Hardware.Detector/Abstractions/Events/CorrectionCompletedEvent.cs
@@ -0,0 +1,14 @@
+using Prism.Events;
+using XP.Hardware.Detector.Abstractions;
+
+namespace XP.Hardware.Detector.Abstractions.Events
+{
+ ///
+ /// 校正完成事件 | Correction completed event
+ /// 当探测器校正操作完成时触发
+ /// 线程安全:使用 Prism EventAggregator 确保线程安全的发布和订阅
+ ///
+ public class CorrectionCompletedEvent : PubSubEvent
+ {
+ }
+}
diff --git a/XP.Hardware.Detector/Abstractions/Events/CorrectionCompletedEventArgs.cs b/XP.Hardware.Detector/Abstractions/Events/CorrectionCompletedEventArgs.cs
new file mode 100644
index 0000000..9ccdf52
--- /dev/null
+++ b/XP.Hardware.Detector/Abstractions/Events/CorrectionCompletedEventArgs.cs
@@ -0,0 +1,33 @@
+using System;
+using XP.Hardware.Detector.Abstractions.Enums;
+
+namespace XP.Hardware.Detector.Abstractions
+{
+ ///
+ /// 校正完成事件参数 | Correction completed event arguments
+ /// 携带校正类型和结果信息
+ ///
+ public class CorrectionCompletedEventArgs : EventArgs
+ {
+ ///
+ /// 校正类型 | Correction type
+ ///
+ public CorrectionType Type { get; set; }
+
+ ///
+ /// 校正结果 | Correction result
+ ///
+ public DetectorResult Result { get; set; }
+
+ ///
+ /// 构造函数 | Constructor
+ ///
+ /// 校正类型 | Correction type
+ /// 校正结果 | Correction result
+ public CorrectionCompletedEventArgs(CorrectionType type, DetectorResult result)
+ {
+ Type = type;
+ Result = result;
+ }
+ }
+}
diff --git a/XP.Hardware.Detector/Abstractions/Events/ErrorOccurredEvent.cs b/XP.Hardware.Detector/Abstractions/Events/ErrorOccurredEvent.cs
new file mode 100644
index 0000000..c67943b
--- /dev/null
+++ b/XP.Hardware.Detector/Abstractions/Events/ErrorOccurredEvent.cs
@@ -0,0 +1,14 @@
+using Prism.Events;
+using XP.Hardware.Detector.Abstractions;
+
+namespace XP.Hardware.Detector.Abstractions.Events
+{
+ ///
+ /// 错误发生事件 | Error occurred event
+ /// 当探测器操作发生错误时触发
+ /// 线程安全:使用 Prism EventAggregator 确保线程安全的发布和订阅
+ ///
+ public class ErrorOccurredEvent : PubSubEvent
+ {
+ }
+}
diff --git a/XP.Hardware.Detector/Abstractions/Events/ImageCapturedEvent.cs b/XP.Hardware.Detector/Abstractions/Events/ImageCapturedEvent.cs
new file mode 100644
index 0000000..f31bc32
--- /dev/null
+++ b/XP.Hardware.Detector/Abstractions/Events/ImageCapturedEvent.cs
@@ -0,0 +1,14 @@
+using Prism.Events;
+using XP.Hardware.Detector.Abstractions;
+
+namespace XP.Hardware.Detector.Abstractions.Events
+{
+ ///
+ /// 图像采集事件 | Image captured event
+ /// 当探测器采集到新图像时触发
+ /// 线程安全:使用 Prism EventAggregator 确保线程安全的发布和订阅
+ ///
+ public class ImageCapturedEvent : PubSubEvent
+ {
+ }
+}
diff --git a/XP.Hardware.Detector/Abstractions/Events/ImageCapturedEventArgs.cs b/XP.Hardware.Detector/Abstractions/Events/ImageCapturedEventArgs.cs
new file mode 100644
index 0000000..76e6b25
--- /dev/null
+++ b/XP.Hardware.Detector/Abstractions/Events/ImageCapturedEventArgs.cs
@@ -0,0 +1,46 @@
+using System;
+
+namespace XP.Hardware.Detector.Abstractions
+{
+ ///
+ /// 图像采集事件参数 | Image captured event arguments
+ /// 携带图像数据和元信息
+ ///
+ public class ImageCapturedEventArgs : EventArgs
+ {
+ ///
+ /// 16 位图像原始数据(无符号)| 16-bit raw image data (unsigned)
+ ///
+ public ushort[] ImageData { get; set; }
+
+ ///
+ /// 图像宽度 | Image width
+ ///
+ public uint Width { get; set; }
+
+ ///
+ /// 图像高度 | Image height
+ ///
+ public uint Height { get; set; }
+
+ ///
+ /// 帧号 | Frame number
+ ///
+ public int FrameNumber { get; set; }
+
+ ///
+ /// 存储路径 | Save path
+ ///
+ public string SavePath { get; set; }
+
+ ///
+ /// 曝光时间(毫秒)| Exposure time (milliseconds)
+ ///
+ public uint ExposureTime { get; set; }
+
+ ///
+ /// 采集时间戳 | Capture timestamp
+ ///
+ public DateTime CaptureTime { get; set; }
+ }
+}
diff --git a/XP.Hardware.Detector/Abstractions/Events/StatusChangedEvent.cs b/XP.Hardware.Detector/Abstractions/Events/StatusChangedEvent.cs
new file mode 100644
index 0000000..33cf8e0
--- /dev/null
+++ b/XP.Hardware.Detector/Abstractions/Events/StatusChangedEvent.cs
@@ -0,0 +1,14 @@
+using Prism.Events;
+using XP.Hardware.Detector.Abstractions.Enums;
+
+namespace XP.Hardware.Detector.Abstractions.Events
+{
+ ///
+ /// 状态变更事件 | Status changed event
+ /// 当探测器状态发生变化时触发
+ /// 线程安全:使用 Prism EventAggregator 确保线程安全的发布和订阅
+ ///
+ public class StatusChangedEvent : PubSubEvent
+ {
+ }
+}
diff --git a/XP.Hardware.Detector/Abstractions/IAreaDetector.cs b/XP.Hardware.Detector/Abstractions/IAreaDetector.cs
new file mode 100644
index 0000000..527742d
--- /dev/null
+++ b/XP.Hardware.Detector/Abstractions/IAreaDetector.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using XP.Hardware.Detector.Abstractions.Enums;
+
+namespace XP.Hardware.Detector.Abstractions
+{
+ ///
+ /// 面阵探测器通用接口 | Area detector common interface
+ /// 定义所有探测器必须实现的核心能力
+ ///
+ public interface IAreaDetector : IDisposable
+ {
+ ///
+ /// 探测器状态 | Detector status
+ ///
+ DetectorStatus Status { get; }
+
+ ///
+ /// 探测器类型 | Detector type
+ ///
+ DetectorType Type { get; }
+
+ ///
+ /// 初始化探测器 | Initialize detector
+ ///
+ /// 取消令牌 | Cancellation token
+ /// 操作结果 | Operation result
+ Task InitializeAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// 启动连续采集 | Start continuous acquisition
+ ///
+ /// 取消令牌 | Cancellation token
+ /// 操作结果 | Operation result
+ Task StartAcquisitionAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// 停止采集 | Stop acquisition
+ ///
+ /// 取消令牌 | Cancellation token
+ /// 操作结果 | Operation result
+ Task StopAcquisitionAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// 单帧采集 | Single frame acquisition
+ ///
+ /// 取消令牌 | Cancellation token
+ /// 操作结果 | Operation result
+ Task AcquireSingleFrameAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// 暗场校正 | Dark field correction
+ ///
+ /// 采集帧数 | Frame count
+ /// 取消令牌 | Cancellation token
+ /// 操作结果 | Operation result
+ Task DarkCorrectionAsync(int frameCount, CancellationToken cancellationToken = default);
+
+ ///
+ /// 增益校正(亮场校正)| Gain correction (bright field correction)
+ ///
+ /// 采集帧数 | Frame count
+ /// 取消令牌 | Cancellation token
+ /// 操作结果 | Operation result
+ Task GainCorrectionAsync(int frameCount, CancellationToken cancellationToken = default);
+
+ ///
+ /// 自动校正(暗场+增益+坏像素)| Auto correction (dark + gain + bad pixel)
+ ///
+ /// 采集帧数 | Frame count
+ /// 取消令牌 | Cancellation token
+ /// 操作结果 | Operation result
+ Task AutoCorrectionAsync(int frameCount, CancellationToken cancellationToken = default);
+
+ ///
+ /// 坏像素校正 | Bad pixel correction
+ ///
+ /// 取消令牌 | Cancellation token
+ /// 操作结果 | Operation result
+ Task BadPixelCorrectionAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// 获取探测器信息 | Get detector information
+ ///
+ /// 探测器信息 | Detector information
+ DetectorInfo GetInfo();
+ }
+}
diff --git a/XP.Hardware.Detector/Abstractions/IIRayDetector.cs b/XP.Hardware.Detector/Abstractions/IIRayDetector.cs
new file mode 100644
index 0000000..6e8eff5
--- /dev/null
+++ b/XP.Hardware.Detector/Abstractions/IIRayDetector.cs
@@ -0,0 +1,38 @@
+using System.Threading.Tasks;
+using XP.Hardware.Detector.Abstractions.Enums;
+
+namespace XP.Hardware.Detector.Abstractions
+{
+ ///
+ /// iRay 探测器专属接口 | iRay detector specific interface
+ /// 扩展 iRay 特有的功能
+ ///
+ public interface IIRayDetector : IAreaDetector
+ {
+ ///
+ /// 设置采集模式 | Set acquisition mode
+ ///
+ /// 采集模式 | Acquisition mode
+ /// 操作结果 | Operation result
+ Task SetAcquisitionModeAsync(AcquisitionMode mode);
+
+ ///
+ /// 获取采集模式 | Get acquisition mode
+ ///
+ /// 当前采集模式 | Current acquisition mode
+ AcquisitionMode GetAcquisitionMode();
+
+ ///
+ /// 设置增益值 | Set gain value
+ ///
+ /// 增益值 | Gain value
+ /// 操作结果 | Operation result
+ Task SetGainAsync(double gain);
+
+ ///
+ /// 获取增益值 | Get gain value
+ ///
+ /// 当前增益值 | Current gain value
+ double GetGain();
+ }
+}
diff --git a/XP.Hardware.Detector/Abstractions/IVarexDetector.cs b/XP.Hardware.Detector/Abstractions/IVarexDetector.cs
new file mode 100644
index 0000000..ff33956
--- /dev/null
+++ b/XP.Hardware.Detector/Abstractions/IVarexDetector.cs
@@ -0,0 +1,67 @@
+using System.Threading.Tasks;
+using XP.Hardware.Detector.Abstractions.Enums;
+
+namespace XP.Hardware.Detector.Abstractions
+{
+ ///
+ /// Varex 探测器专属接口 | Varex detector specific interface
+ /// 扩展 Varex 特有的功能
+ ///
+ public interface IVarexDetector : IAreaDetector
+ {
+ ///
+ /// 设置 Binning 模式 | Set binning mode
+ ///
+ /// Binning 模式 | Binning mode
+ /// 操作结果 | Operation result
+ Task SetBinningModeAsync(BinningMode mode);
+
+ ///
+ /// 获取 Binning 模式 | Get binning mode
+ ///
+ /// 当前 Binning 模式 | Current binning mode
+ BinningMode GetBinningMode();
+
+ ///
+ /// 设置增益模式 | Set gain mode
+ ///
+ /// 增益模式 | Gain mode
+ /// 操作结果 | Operation result
+ Task SetGainModeAsync(GainMode mode);
+
+ ///
+ /// 获取增益模式 | Get gain mode
+ ///
+ /// 当前增益模式 | Current gain mode
+ GainMode GetGainMode();
+
+ ///
+ /// 设置曝光时间 | Set exposure time
+ ///
+ /// 曝光时间(毫秒)| Exposure time (milliseconds)
+ /// 操作结果 | Operation result
+ Task SetExposureTimeAsync(uint milliseconds);
+
+ ///
+ /// 获取曝光时间 | Get exposure time
+ ///
+ /// 曝光时间(毫秒)| Exposure time (milliseconds)
+ uint GetExposureTime();
+
+ ///
+ /// 设置 ROI 区域 | Set ROI region
+ ///
+ /// 起始 X 坐标 | Start X coordinate
+ /// 起始 Y 坐标 | Start Y coordinate
+ /// 宽度 | Width
+ /// 高度 | Height
+ /// 操作结果 | Operation result
+ Task SetROIAsync(uint x, uint y, uint width, uint height);
+
+ ///
+ /// 获取 ROI 区域 | Get ROI region
+ ///
+ /// ROI 参数 | ROI parameters
+ (uint x, uint y, uint width, uint height) GetROI();
+ }
+}
diff --git a/XP.Hardware.Detector/Config/ConfigLoader.cs b/XP.Hardware.Detector/Config/ConfigLoader.cs
new file mode 100644
index 0000000..bc9d512
--- /dev/null
+++ b/XP.Hardware.Detector/Config/ConfigLoader.cs
@@ -0,0 +1,245 @@
+using System;
+using System.Configuration;
+using System.IO;
+using System.Net;
+using XP.Hardware.Detector.Abstractions;
+using XP.Hardware.Detector.Abstractions.Enums;
+
+namespace XP.Hardware.Detector.Config
+{
+ ///
+ /// 配置加载器 | Configuration loader
+ /// 从 App.config 读取探测器配置并验证参数有效性
+ ///
+ public static class ConfigLoader
+ {
+ ///
+ /// 加载配置 | Load configuration
+ ///
+ /// 配置结果 | Configuration result
+ public static DetectorResult LoadConfiguration()
+ {
+ try
+ {
+ // 读取探测器类型 | Read detector type
+ string typeStr = ConfigurationManager.AppSettings["Detector:Type"];
+ if (string.IsNullOrEmpty(typeStr))
+ {
+ return DetectorResult.Failure("配置文件中未找到 Detector:Type | Detector:Type not found in configuration");
+ }
+
+ if (!Enum.TryParse(typeStr, true, out var detectorType))
+ {
+ return DetectorResult.Failure($"无效的探测器类型:{typeStr} | Invalid detector type: {typeStr}");
+ }
+
+ // 根据类型创建配置对象 | Create configuration object based on type
+ DetectorConfig config = detectorType switch
+ {
+ DetectorType.Varex => LoadVarexConfiguration(),
+ DetectorType.IRay => LoadIRayConfiguration(),
+ _ => throw new NotSupportedException($"不支持的探测器类型:{detectorType} | Unsupported detector type: {detectorType}")
+ };
+
+ // 加载通用配置 | Load common configuration
+ config.Type = detectorType;
+ config.IP = ConfigurationManager.AppSettings["Detector:IP"] ?? "127.0.0.1";
+ config.Port = int.TryParse(ConfigurationManager.AppSettings["Detector:Port"], out var port) ? port : 5000;
+ config.SavePath = ConfigurationManager.AppSettings["Detector:SavePath"] ?? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Images");
+ config.AutoSave = bool.TryParse(ConfigurationManager.AppSettings["Detector:AutoSave"], out var autoSave) && autoSave;
+
+ // 验证配置 | Validate configuration
+ var validationResult = ValidateConfiguration(config);
+ if (!validationResult.IsSuccess)
+ {
+ return DetectorResult.Failure(validationResult.ErrorMessage);
+ }
+
+ return DetectorResult.Success(config, "配置加载成功 | Configuration loaded successfully");
+ }
+ catch (Exception ex)
+ {
+ return DetectorResult.Failure($"加载配置异常 | Load configuration exception: {ex.Message}", ex);
+ }
+ }
+
+ ///
+ /// 加载 Varex 配置 | Load Varex configuration
+ ///
+ private static VarexDetectorConfig LoadVarexConfiguration()
+ {
+ var config = new VarexDetectorConfig();
+
+ // 读取 Binning 模式 | Read binning mode
+ string binningStr = ConfigurationManager.AppSettings["Detector:Varex:BinningMode"];
+ if (!string.IsNullOrEmpty(binningStr) && Enum.TryParse(binningStr, true, out var binningMode))
+ {
+ config.BinningMode = binningMode;
+ }
+
+ // 读取增益模式 | Read gain mode
+ string gainStr = ConfigurationManager.AppSettings["Detector:Varex:GainMode"];
+ if (!string.IsNullOrEmpty(gainStr) && Enum.TryParse(gainStr, true, out var gainMode))
+ {
+ config.GainMode = gainMode;
+ }
+
+ // 读取曝光时间 | Read exposure time
+ if (uint.TryParse(ConfigurationManager.AppSettings["Detector:Varex:ExposureTime"], out var exposureTime))
+ {
+ config.ExposureTime = exposureTime;
+ }
+
+ // 读取 ROI 参数 | Read ROI parameters
+ if (uint.TryParse(ConfigurationManager.AppSettings["Detector:Varex:ROI_X"], out var roiX))
+ {
+ config.RoiX = roiX;
+ }
+ if (uint.TryParse(ConfigurationManager.AppSettings["Detector:Varex:ROI_Y"], out var roiY))
+ {
+ config.RoiY = roiY;
+ }
+ if (uint.TryParse(ConfigurationManager.AppSettings["Detector:Varex:ROI_Width"], out var roiWidth))
+ {
+ config.RoiWidth = roiWidth;
+ }
+ if (uint.TryParse(ConfigurationManager.AppSettings["Detector:Varex:ROI_Height"], out var roiHeight))
+ {
+ config.RoiHeight = roiHeight;
+ }
+
+ return config;
+ }
+
+ ///
+ /// 加载 iRay 配置 | Load iRay configuration
+ ///
+ private static IRayDetectorConfig LoadIRayConfiguration()
+ {
+ var config = new IRayDetectorConfig();
+
+ // 读取默认增益 | Read default gain
+ if (double.TryParse(ConfigurationManager.AppSettings["Detector:IRay:DefaultGain"], out var defaultGain))
+ {
+ config.DefaultGain = defaultGain;
+ }
+
+ // 读取采集模式 | Read acquisition mode
+ string modeStr = ConfigurationManager.AppSettings["Detector:IRay:AcquisitionMode"];
+ if (!string.IsNullOrEmpty(modeStr) && Enum.TryParse(modeStr, true, out var acquisitionMode))
+ {
+ config.AcquisitionMode = acquisitionMode;
+ }
+
+ return config;
+ }
+
+ ///
+ /// 保存探测器参数到 App.config | Save detector parameters to App.config
+ ///
+ /// Binning 索引 | Binning index
+ /// PGA 灵敏度值 | PGA sensitivity value
+ /// 帧率 | Frame rate
+ /// 帧合并数 | Average frame count
+ public static void SaveParameters(int binningIndex, int pga, decimal frameRate, int avgFrames)
+ {
+ var config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
+
+ SetAppSetting(config, "Detector:BinningIndex", binningIndex.ToString());
+ SetAppSetting(config, "Detector:PGA", pga.ToString());
+ SetAppSetting(config, "Detector:FrameRate", frameRate.ToString(System.Globalization.CultureInfo.InvariantCulture));
+ SetAppSetting(config, "Detector:AvgFrames", avgFrames.ToString());
+
+ config.Save(ConfigurationSaveMode.Modified);
+ ConfigurationManager.RefreshSection("appSettings");
+ }
+
+ ///
+ /// 加载已保存的探测器参数 | Load saved detector parameters
+ ///
+ /// 参数元组(binningIndex, pga, frameRate, avgFrames),加载失败返回 null | Parameter tuple, null if failed
+ public static (int binningIndex, int pga, decimal frameRate, int avgFrames)? LoadSavedParameters()
+ {
+ try
+ {
+ string binStr = ConfigurationManager.AppSettings["Detector:BinningIndex"];
+ string pgaStr = ConfigurationManager.AppSettings["Detector:PGA"];
+ string frStr = ConfigurationManager.AppSettings["Detector:FrameRate"];
+ string avgStr = ConfigurationManager.AppSettings["Detector:AvgFrames"];
+
+ if (string.IsNullOrEmpty(binStr) || string.IsNullOrEmpty(pgaStr) ||
+ string.IsNullOrEmpty(frStr) || string.IsNullOrEmpty(avgStr))
+ {
+ return null;
+ }
+
+ if (int.TryParse(binStr, out var binning) &&
+ int.TryParse(pgaStr, out var pgaVal) &&
+ decimal.TryParse(frStr, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var fr) &&
+ int.TryParse(avgStr, out var avg))
+ {
+ return (binning, pgaVal, fr, avg);
+ }
+ }
+ catch
+ {
+ // 忽略加载异常 | Ignore load exception
+ }
+ return null;
+ }
+
+ ///
+ /// 设置 AppSettings 键值 | Set AppSettings key-value
+ ///
+ private static void SetAppSetting(Configuration config, string key, string value)
+ {
+ if (config.AppSettings.Settings[key] != null)
+ {
+ config.AppSettings.Settings[key].Value = value;
+ }
+ else
+ {
+ config.AppSettings.Settings.Add(key, value);
+ }
+ }
+
+ ///
+ /// 验证配置参数 | Validate configuration parameters
+ ///
+ private static DetectorResult ValidateConfiguration(DetectorConfig config)
+ {
+ // 验证 IP 地址 | Validate IP address
+ if (!IPAddress.TryParse(config.IP, out _))
+ {
+ return DetectorResult.Failure($"无效的 IP 地址:{config.IP} | Invalid IP address: {config.IP}");
+ }
+
+ // 验证端口范围 | Validate port range
+ if (config.Port < 1 || config.Port > 65535)
+ {
+ return DetectorResult.Failure($"无效的端口号:{config.Port},必须在 1-65535 之间 | Invalid port: {config.Port}, must be between 1-65535");
+ }
+
+ // 验证存储路径 | Validate save path
+ if (string.IsNullOrWhiteSpace(config.SavePath))
+ {
+ return DetectorResult.Failure("存储路径不能为空 | Save path cannot be empty");
+ }
+
+ try
+ {
+ // 尝试创建目录以验证路径有效性 | Try to create directory to validate path
+ if (!Directory.Exists(config.SavePath))
+ {
+ Directory.CreateDirectory(config.SavePath);
+ }
+ }
+ catch (Exception ex)
+ {
+ return DetectorResult.Failure($"无效的存储路径:{config.SavePath},错误:{ex.Message} | Invalid save path: {config.SavePath}, error: {ex.Message}");
+ }
+
+ return DetectorResult.Success("配置验证通过 | Configuration validation passed");
+ }
+ }
+}
diff --git a/XP.Hardware.Detector/Config/DetectorConfig.cs b/XP.Hardware.Detector/Config/DetectorConfig.cs
new file mode 100644
index 0000000..d0c437d
--- /dev/null
+++ b/XP.Hardware.Detector/Config/DetectorConfig.cs
@@ -0,0 +1,138 @@
+using System.Collections.Generic;
+using XP.Hardware.Detector.Abstractions.Enums;
+
+namespace XP.Hardware.Detector.Config
+{
+ ///
+ /// 探测器通用配置基类 | Detector common configuration base class
+ /// 包含所有探测器的通用配置参数
+ ///
+ public class DetectorConfig
+ {
+ ///
+ /// 探测器类型 | Detector type
+ ///
+ public DetectorType Type { get; set; }
+
+ ///
+ /// IP 地址 | IP address
+ ///
+ public string IP { get; set; }
+
+ ///
+ /// 端口号 | Port number
+ ///
+ public int Port { get; set; }
+
+ ///
+ /// 图像存储路径 | Image save path
+ ///
+ public string SavePath { get; set; }
+
+ ///
+ /// 是否自动保存图像 | Whether to auto save images
+ ///
+ public bool AutoSave { get; set; }
+
+ ///
+ /// 获取支持的 Binning 选项(显示名称 → 索引)| Get supported binning options (display name → index)
+ /// 子类可重写以提供不同的选项列表
+ ///
+ public virtual List GetSupportedBinnings()
+ {
+ return new List
+ {
+ new BinningOption("1×1", 0),
+ new BinningOption("2×2", 1),
+ };
+ }
+
+ ///
+ /// 获取支持的 PGA(灵敏度)选项 | Get supported PGA (sensitivity) options
+ /// 子类可重写以提供不同的选项列表
+ ///
+ public virtual List GetSupportedPgaValues()
+ {
+ return new List { 2, 3, 4, 5, 6, 7 };
+ }
+
+ ///
+ /// 获取各 Binning 模式下的最大帧率 | Get max frame rate for each binning mode
+ /// 子类可重写以提供不同的限制
+ ///
+ public virtual decimal GetMaxFrameRate(int binningIndex)
+ {
+ return 15m;
+ }
+
+ ///
+ /// 获取指定 Binning 模式下的图像规格(像素尺寸、分辨率)| Get image spec for given binning mode
+ /// 子类可重写以提供不同的映射关系
+ ///
+ /// Binning 索引 | Binning index
+ /// 图像规格 | Image specification
+ public virtual BinningImageSpec GetImageSpec(int binningIndex)
+ {
+ return new BinningImageSpec(0.139, 0.139, 3072, 3060);
+ }
+ }
+
+ ///
+ /// Binning 模式下的图像规格 | Image specification for binning mode
+ /// 包含像素尺寸和图像分辨率,供重建 PC 使用
+ ///
+ public class BinningImageSpec
+ {
+ ///
+ /// X 方向像素尺寸(mm)| Pixel size in X direction (mm)
+ ///
+ public double PixelX { get; }
+
+ ///
+ /// Y 方向像素尺寸(mm)| Pixel size in Y direction (mm)
+ ///
+ public double PixelY { get; }
+
+ ///
+ /// 图像宽度(像素)| Image width (pixels)
+ ///
+ public int ImageWidth { get; }
+
+ ///
+ /// 图像高度(像素)| Image height (pixels)
+ ///
+ public int ImageHeight { get; }
+
+ public BinningImageSpec(double pixelX, double pixelY, int imageWidth, int imageHeight)
+ {
+ PixelX = pixelX;
+ PixelY = pixelY;
+ ImageWidth = imageWidth;
+ ImageHeight = imageHeight;
+ }
+ }
+
+ ///
+ /// Binning 选项模型 | Binning option model
+ ///
+ public class BinningOption
+ {
+ ///
+ /// 显示名称 | Display name
+ ///
+ public string DisplayName { get; }
+
+ ///
+ /// 索引值 | Index value
+ ///
+ public int Index { get; }
+
+ public BinningOption(string displayName, int index)
+ {
+ DisplayName = displayName;
+ Index = index;
+ }
+
+ public override string ToString() => DisplayName;
+ }
+}
diff --git a/XP.Hardware.Detector/Config/IRayDetectorConfig.cs b/XP.Hardware.Detector/Config/IRayDetectorConfig.cs
new file mode 100644
index 0000000..4ec7aa5
--- /dev/null
+++ b/XP.Hardware.Detector/Config/IRayDetectorConfig.cs
@@ -0,0 +1,56 @@
+using System.Collections.Generic;
+using XP.Hardware.Detector.Abstractions.Enums;
+
+namespace XP.Hardware.Detector.Config
+{
+ ///
+ /// iRay 探测器配置类 | iRay detector configuration class
+ /// 包含 iRay 专属的配置参数
+ ///
+ public class IRayDetectorConfig : DetectorConfig
+ {
+ ///
+ /// 默认增益值 | Default gain value
+ ///
+ public double DefaultGain { get; set; } = 1.0;
+
+ ///
+ /// 采集模式 | Acquisition mode
+ ///
+ public AcquisitionMode AcquisitionMode { get; set; } = AcquisitionMode.Continuous;
+
+ ///
+ /// iRay 支持 1×1、2×2、3×3、4×4 四种 Binning | iRay supports 1×1, 2×2, 3×3, 4×4 binning
+ ///
+ public override List GetSupportedBinnings()
+ {
+ return new List
+ {
+ new BinningOption("1×1", 0),
+ new BinningOption("2×2", 1),
+ new BinningOption("3×3", 2),
+ new BinningOption("4×4", 3),
+ };
+ }
+
+ ///
+ /// iRay PGA 范围 1-8 | iRay PGA range 1-8
+ ///
+ public override List GetSupportedPgaValues()
+ {
+ return new List { 1, 2, 3, 4, 5, 6, 7, 8 };
+ }
+
+ ///
+ /// iRay 各 Binning 模式最大帧率 | iRay max frame rate per binning mode
+ ///
+ public override decimal GetMaxFrameRate(int binningIndex) => binningIndex switch
+ {
+ 0 => 15m, // 1×1
+ 1 => 30m, // 2×2
+ 2 => 45m, // 3×3
+ 3 => 60m, // 4×4
+ _ => 15m
+ };
+ }
+}
diff --git a/XP.Hardware.Detector/Config/VarexDetectorConfig.cs b/XP.Hardware.Detector/Config/VarexDetectorConfig.cs
new file mode 100644
index 0000000..66230ab
--- /dev/null
+++ b/XP.Hardware.Detector/Config/VarexDetectorConfig.cs
@@ -0,0 +1,94 @@
+using System.Collections.Generic;
+using XP.Hardware.Detector.Abstractions.Enums;
+
+namespace XP.Hardware.Detector.Config
+{
+ ///
+ /// Varex 探测器配置类 | Varex detector configuration class
+ /// 包含 Varex 专属的配置参数
+ ///
+ public class VarexDetectorConfig : DetectorConfig
+ {
+ ///
+ /// Binning 模式 | Binning mode
+ ///
+ public BinningMode BinningMode { get; set; } = BinningMode.Bin1x1;
+
+ ///
+ /// 增益模式 | Gain mode
+ ///
+ public GainMode GainMode { get; set; } = GainMode.Low;
+
+ ///
+ /// 曝光时间(毫秒)| Exposure time (milliseconds)
+ ///
+ public uint ExposureTime { get; set; } = 100;
+
+ ///
+ /// ROI 起始 X 坐标 | ROI start X coordinate
+ ///
+ public uint RoiX { get; set; } = 0;
+
+ ///
+ /// ROI 起始 Y 坐标 | ROI start Y coordinate
+ ///
+ public uint RoiY { get; set; } = 0;
+
+ ///
+ /// ROI 宽度 | ROI width
+ ///
+ public uint RoiWidth { get; set; } = 0;
+
+ ///
+ /// ROI 高度 | ROI height
+ ///
+ public uint RoiHeight { get; set; } = 0;
+
+ ///
+ /// Varex 支持 1×1、2×2、4×4 三种 Binning | Varex supports 1×1, 2×2, 4×4 binning
+ ///
+ public override List GetSupportedBinnings()
+ {
+ return new List
+ {
+ new BinningOption("1×1", 0),
+ new BinningOption("2×2", 1),
+ new BinningOption("3×3", 2),
+ new BinningOption("4×4", 3),
+ };
+ }
+
+ ///
+ /// Varex PGA 范围 2-7 | Varex PGA range 2-7
+ ///
+ public override List GetSupportedPgaValues()
+ {
+ return new List { 2, 3, 4, 5, 6, 7 };
+ }
+
+ ///
+ /// Varex 各 Binning 模式最大帧率 | Varex max frame rate per binning mode
+ ///
+ public override decimal GetMaxFrameRate(int binningIndex) => binningIndex switch
+ {
+ 0 => 15m, // 1×1
+ 1 => 30m, // 2×2
+ 2 => 45m, // 3×3
+ 3 => 60m, // 4×4
+ _ => 15m
+ };
+
+ ///
+ /// Varex 4343N 各 Binning 模式的图像规格 | Varex 4343N image spec per binning mode
+ /// 像素尺寸和分辨率映射关系,供重建 PC 使用
+ ///
+ public override BinningImageSpec GetImageSpec(int binningIndex) => binningIndex switch
+ {
+ 0 => new BinningImageSpec(0.139, 0.139, 3072, 3060), // 1×1
+ 1 => new BinningImageSpec(0.278, 0.278, 1536, 1530), // 2×2
+ 2 => new BinningImageSpec(0.417, 0.417, 1024, 1020), // 3×3
+ 3 => new BinningImageSpec(0.556, 0.556, 768, 765), // 4×4
+ _ => new BinningImageSpec(0.139, 0.139, 3072, 3060)
+ };
+ }
+}
diff --git a/XP.Hardware.Detector/Documents/App.config.example b/XP.Hardware.Detector/Documents/App.config.example
new file mode 100644
index 0000000..e2acfa6
--- /dev/null
+++ b/XP.Hardware.Detector/Documents/App.config.example
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/XP.Hardware.Detector/Documents/GUIDENCE.md b/XP.Hardware.Detector/Documents/GUIDENCE.md
new file mode 100644
index 0000000..405647e
--- /dev/null
+++ b/XP.Hardware.Detector/Documents/GUIDENCE.md
@@ -0,0 +1,1031 @@
+# 探测器模块使用指南 | Detector Module Usage Guide
+
+## 1. 模块概述 | Module Overview
+
+XP.Hardware.Detector 是 XplorePlane X 射线检测系统的探测器控制模块,负责与工业 X 射线探测器进行通讯和控制。该模块采用策略模式设计,支持多种探测器型号的统一管理。
+
+### 核心特性 | Key Features
+
+- 支持多种探测器型号(Varex、iRay 等)
+- 完整的探测器生命周期管理(初始化、采集、校正)
+- 单帧和连续采集模式
+- 自动校正功能(暗场、增益、坏像素)
+- 实时状态监控
+- 基于 Prism 事件聚合器的松耦合通讯
+- 异步操作,不阻塞 UI 线程
+
+---
+
+## 2. 模块注册 | Module Registration
+
+探测器模块通过 Prism 的模块化系统注册,在应用启动时自动加载。
+
+### 在 App.xaml.cs 中注册
+
+```csharp
+using Prism.Modularity;
+using XP.Hardware.Detector.Module;
+
+protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
+{
+ // 注册探测器模块 | Register Detector module
+ moduleCatalog.AddModule();
+
+ base.ConfigureModuleCatalog(moduleCatalog);
+}
+```
+
+### 模块自动注册的服务
+
+- `IDetectorFactory` - 探测器工厂(瞬态)
+- `IDetectorService` - 探测器业务服务(单例)
+
+---
+
+## 3. 配置文件设置 | Configuration File Setup
+
+在 `App.config` 中添加探测器配置:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### 配置参数说明
+
+| 参数 | 类型 | 默认值 | 说明 |
+|------|------|--------|------|
+| Type | string | Varex | 探测器类型(Varex/IRay)|
+| IP | string | 192.168.1.200 | 探测器 IP 地址 |
+| Port | int | 50000 | 探测器端口号 |
+| SavePath | string | D:\Images | 图像存储路径 |
+| AutoSave | bool | true | 是否自动保存图像 |
+
+---
+## 4. 在 ViewModel 中使用 | Usage in ViewModel
+
+### 4.1 注入服务
+
+```csharp
+using Prism.Commands;
+using Prism.Events;
+using Prism.Mvvm;
+using System.Threading.Tasks;
+using XP.Common.Logging.Interfaces;
+using XP.Hardware.Detector.Abstractions;
+using XP.Hardware.Detector.Abstractions.Enums;
+using XP.Hardware.Detector.Abstractions.Events;
+using XP.Hardware.Detector.Services;
+
+namespace YourNamespace.ViewModels
+{
+ public class YourViewModel : BindableBase
+ {
+ private readonly IDetectorService _detectorService;
+ private readonly IEventAggregator _eventAggregator;
+ private readonly ILoggerService _logger;
+
+ ///
+ /// 构造函数,通过依赖注入获取服务 | Constructor with dependency injection
+ ///
+ public YourViewModel(
+ IDetectorService detectorService,
+ IEventAggregator eventAggregator,
+ ILoggerService logger)
+ {
+ _detectorService = detectorService;
+ _eventAggregator = eventAggregator;
+ _logger = logger.ForModule("YourViewModel");
+
+ // 订阅事件 | Subscribe to events
+ SubscribeEvents();
+ }
+
+ private void SubscribeEvents()
+ {
+ // 订阅图像采集事件 | Subscribe to image captured event
+ _eventAggregator.GetEvent()
+ .Subscribe(OnImageCaptured);
+
+ // 订阅校正完成事件 | Subscribe to correction completed event
+ _eventAggregator.GetEvent()
+ .Subscribe(OnCorrectionCompleted);
+ }
+
+ private void OnImageCaptured(ImageCapturedEventArgs args)
+ {
+ _logger.Info($"图像已采集: {args.FrameNumber}");
+ }
+
+ private void OnCorrectionCompleted(CorrectionCompletedEventArgs args)
+ {
+ _logger.Info($"校正完成: {args.CorrectionType}");
+ }
+ }
+}
+```
+
+### 4.2 初始化探测器
+
+```csharp
+///
+/// 初始化探测器 | Initialize detector
+///
+public async Task InitializeDetectorAsync()
+{
+ try
+ {
+ _logger.Info("开始初始化探测器... | Starting detector initialization...");
+ StatusMessage = "正在初始化... | Initializing...";
+
+ DetectorResult result = await _detectorService.InitializeAsync();
+
+ if (result.IsSuccess)
+ {
+ _logger.Info("探测器初始化成功 | Detector initialized successfully");
+ StatusMessage = "探测器已就绪 | Detector ready";
+
+ // 获取探测器信息 | Get detector information
+ DetectorInfo info = _detectorService.GetInfo();
+ _logger.Info($"探测器型号: {info.Model}, 分辨率: {info.MaxWidth}x{info.MaxHeight}");
+ }
+ else
+ {
+ _logger.Error(null, $"探测器初始化失败: {result.ErrorMessage}");
+ StatusMessage = $"初始化失败: {result.ErrorMessage}";
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, $"初始化异常: {ex.Message}");
+ StatusMessage = $"初始化异常: {ex.Message}";
+ }
+}
+```
+
+### 4.3 单帧采集
+
+```csharp
+///
+/// 单帧采集 | Single frame acquisition
+///
+public async Task AcquireSingleFrameAsync()
+{
+ try
+ {
+ if (_detectorService.Status != DetectorStatus.Ready)
+ {
+ _logger.Warn("探测器未就绪,无法采集");
+ StatusMessage = "探测器未就绪 | Detector not ready";
+ return;
+ }
+
+ _logger.Info("开始单帧采集... | Starting single frame acquisition...");
+ StatusMessage = "正在采集... | Acquiring...";
+
+ DetectorResult result = await _detectorService.AcquireSingleFrameAsync();
+
+ if (result.IsSuccess)
+ {
+ _logger.Info("单帧采集成功 | Single frame acquired successfully");
+ StatusMessage = "采集完成 | Acquisition completed";
+ }
+ else
+ {
+ _logger.Error(null, $"单帧采集失败: {result.ErrorMessage}");
+ StatusMessage = $"采集失败: {result.ErrorMessage}";
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, $"单帧采集异常: {ex.Message}");
+ StatusMessage = $"采集异常: {ex.Message}";
+ }
+}
+```
+
+### 4.4 启动连续采集
+
+```csharp
+///
+/// 启动连续采集 | Start continuous acquisition
+///
+public async Task StartAcquisitionAsync()
+{
+ try
+ {
+ if (_detectorService.Status != DetectorStatus.Ready)
+ {
+ _logger.Warn("探测器未就绪,无法启动采集");
+ return;
+ }
+
+ _logger.Info("启动连续采集... | Starting continuous acquisition...");
+ StatusMessage = "连续采集中... | Continuous acquisition...";
+
+ DetectorResult result = await _detectorService.StartAcquisitionAsync();
+
+ if (result.IsSuccess)
+ {
+ _logger.Info("连续采集已启动 | Continuous acquisition started");
+ IsAcquiring = true;
+ }
+ else
+ {
+ _logger.Error(null, $"启动采集失败: {result.ErrorMessage}");
+ StatusMessage = $"启动失败: {result.ErrorMessage}";
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, $"启动采集异常: {ex.Message}");
+ StatusMessage = $"启动异常: {ex.Message}";
+ }
+}
+```
+
+### 4.5 停止采集
+
+```csharp
+///
+/// 停止采集 | Stop acquisition
+///
+public async Task StopAcquisitionAsync()
+{
+ try
+ {
+ _logger.Info("停止采集... | Stopping acquisition...");
+ StatusMessage = "正在停止... | Stopping...";
+
+ DetectorResult result = await _detectorService.StopAcquisitionAsync();
+
+ if (result.IsSuccess)
+ {
+ _logger.Info("采集已停止 | Acquisition stopped");
+ StatusMessage = "已停止 | Stopped";
+ IsAcquiring = false;
+ }
+ else
+ {
+ _logger.Error(null, $"停止采集失败: {result.ErrorMessage}");
+ StatusMessage = $"停止失败: {result.ErrorMessage}";
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, $"停止采集异常: {ex.Message}");
+ StatusMessage = $"停止异常: {ex.Message}";
+ }
+}
+```
+
+---
+## 5. 校正功能 | Correction Functions
+
+### 5.1 自动校正
+
+```csharp
+///
+/// 执行自动校正(暗场+增益+坏像素)| Execute auto correction (dark + gain + bad pixel)
+///
+/// 采集帧数 | Frame count
+public async Task AutoCorrectionAsync(int frameCount = 10)
+{
+ try
+ {
+ _logger.Info($"开始自动校正,帧数: {frameCount} | Starting auto correction, frames: {frameCount}");
+ StatusMessage = "正在校正... | Correcting...";
+
+ DetectorResult result = await _detectorService.AutoCorrectionAsync(frameCount);
+
+ if (result.IsSuccess)
+ {
+ _logger.Info("自动校正完成 | Auto correction completed");
+ StatusMessage = "校正完成 | Correction completed";
+ }
+ else
+ {
+ _logger.Error(null, $"自动校正失败: {result.ErrorMessage}");
+ StatusMessage = $"校正失败: {result.ErrorMessage}";
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, $"自动校正异常: {ex.Message}");
+ StatusMessage = $"校正异常: {ex.Message}";
+ }
+}
+```
+
+### 5.2 获取探测器信息
+
+```csharp
+///
+/// 获取探测器信息 | Get detector information
+///
+public void GetDetectorInfo()
+{
+ try
+ {
+ DetectorInfo info = _detectorService.GetInfo();
+
+ if (info != null)
+ {
+ _logger.Info($"探测器信息:");
+ _logger.Info($" 型号: {info.Model}");
+ _logger.Info($" 序列号: {info.SerialNumber}");
+ _logger.Info($" 固件版本: {info.FirmwareVersion}");
+ _logger.Info($" 分辨率: {info.MaxWidth}x{info.MaxHeight}");
+ _logger.Info($" 像素尺寸: {info.PixelSize}μm");
+ _logger.Info($" 位深度: {info.BitDepth}bit");
+
+ // 更新 UI 显示 | Update UI display
+ DetectorModel = info.Model;
+ Resolution = $"{info.MaxWidth}x{info.MaxHeight}";
+ PixelSize = $"{info.PixelSize}μm";
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, $"获取探测器信息异常: {ex.Message}");
+ }
+}
+```
+
+---
+
+## 6. Prism 事件通讯 | Prism Event Communication
+
+模块使用 Prism 事件聚合器实现跨模块通讯,支持以下事件:
+
+### 6.1 图像采集事件
+
+```csharp
+// 订阅图像采集事件 | Subscribe to image captured event
+_eventAggregator.GetEvent()
+ .Subscribe(OnImageCaptured, ThreadOption.UIThread);
+
+private void OnImageCaptured(ImageCapturedEventArgs args)
+{
+ // args.FrameNumber - 帧编号
+ // args.Timestamp - 时间戳
+ // args.ImageData - 图像数据(如果有)
+
+ FrameNumber = args.FrameNumber;
+ LastCaptureTime = args.Timestamp;
+
+ _logger.Info($"图像已采集: 帧 {args.FrameNumber}");
+}
+```
+
+### 6.2 校正完成事件
+
+```csharp
+// 订阅校正完成事件 | Subscribe to correction completed event
+_eventAggregator.GetEvent()
+ .Subscribe(OnCorrectionCompleted, ThreadOption.UIThread);
+
+private void OnCorrectionCompleted(CorrectionCompletedEventArgs args)
+{
+ // args.CorrectionType - 校正类型(暗场/增益/坏像素)
+ // args.IsSuccess - 是否成功
+ // args.Message - 消息
+
+ if (args.IsSuccess)
+ {
+ _logger.Info($"校正完成: {args.CorrectionType}");
+ StatusMessage = $"{args.CorrectionType} 校正完成";
+ }
+ else
+ {
+ _logger.Error(null, $"校正失败: {args.Message}");
+ StatusMessage = $"校正失败: {args.Message}";
+ }
+}
+```
+
+---
+
+## 7. 完整示例 | Complete Example
+
+```csharp
+using Prism.Commands;
+using Prism.Events;
+using Prism.Mvvm;
+using System;
+using System.Threading.Tasks;
+using System.Windows.Input;
+using XP.Common.Logging.Interfaces;
+using XP.Hardware.Detector.Abstractions;
+using XP.Hardware.Detector.Abstractions.Enums;
+using XP.Hardware.Detector.Abstractions.Events;
+using XP.Hardware.Detector.Services;
+
+namespace YourNamespace.ViewModels
+{
+ public class DetectorControlViewModel : BindableBase
+ {
+ private readonly IDetectorService _detectorService;
+ private readonly IEventAggregator _eventAggregator;
+ private readonly ILoggerService _logger;
+
+ #region 属性 | Properties
+
+ private string _statusMessage;
+ public string StatusMessage
+ {
+ get => _statusMessage;
+ set => SetProperty(ref _statusMessage, value);
+ }
+
+ private DetectorStatus _detectorStatus;
+ public DetectorStatus DetectorStatus
+ {
+ get => _detectorStatus;
+ set => SetProperty(ref _detectorStatus, value);
+ }
+
+ private bool _isAcquiring;
+ public bool IsAcquiring
+ {
+ get => _isAcquiring;
+ set => SetProperty(ref _isAcquiring, value);
+ }
+
+ private int _frameNumber;
+ public int FrameNumber
+ {
+ get => _frameNumber;
+ set => SetProperty(ref _frameNumber, value);
+ }
+
+ private string _detectorModel;
+ public string DetectorModel
+ {
+ get => _detectorModel;
+ set => SetProperty(ref _detectorModel, value);
+ }
+
+ private string _resolution;
+ public string Resolution
+ {
+ get => _resolution;
+ set => SetProperty(ref _resolution, value);
+ }
+
+ #endregion
+
+ #region 命令 | Commands
+
+ public ICommand InitializeCommand { get; }
+ public ICommand AcquireSingleFrameCommand { get; }
+ public ICommand StartAcquisitionCommand { get; }
+ public ICommand StopAcquisitionCommand { get; }
+ public ICommand AutoCorrectionCommand { get; }
+ public ICommand GetInfoCommand { get; }
+
+ #endregion
+
+ public DetectorControlViewModel(
+ IDetectorService detectorService,
+ IEventAggregator eventAggregator,
+ ILoggerService logger)
+ {
+ _detectorService = detectorService;
+ _eventAggregator = eventAggregator;
+ _logger = logger.ForModule("DetectorControlViewModel");
+
+ // 初始化命令 | Initialize commands
+ InitializeCommand = new DelegateCommand(async () => await InitializeAsync());
+ AcquireSingleFrameCommand = new DelegateCommand(async () => await AcquireSingleFrameAsync(), CanAcquire)
+ .ObservesProperty(() => DetectorStatus);
+ StartAcquisitionCommand = new DelegateCommand(async () => await StartAcquisitionAsync(), CanStartAcquisition)
+ .ObservesProperty(() => DetectorStatus)
+ .ObservesProperty(() => IsAcquiring);
+ StopAcquisitionCommand = new DelegateCommand(async () => await StopAcquisitionAsync(), CanStopAcquisition)
+ .ObservesProperty(() => IsAcquiring);
+ AutoCorrectionCommand = new DelegateCommand(async () => await AutoCorrectionAsync(), CanCorrect)
+ .ObservesProperty(() => DetectorStatus);
+ GetInfoCommand = new DelegateCommand(GetInfo);
+
+ // 订阅事件 | Subscribe to events
+ SubscribeEvents();
+ }
+
+ #region 命令实现 | Command Implementations
+
+ private async Task InitializeAsync()
+ {
+ try
+ {
+ StatusMessage = "正在初始化... | Initializing...";
+ DetectorResult result = await _detectorService.InitializeAsync();
+
+ if (result.IsSuccess)
+ {
+ DetectorStatus = _detectorService.Status;
+ StatusMessage = "探测器已就绪 | Detector ready";
+ GetInfo();
+ }
+ else
+ {
+ StatusMessage = $"初始化失败: {result.ErrorMessage}";
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "初始化异常");
+ StatusMessage = $"初始化异常: {ex.Message}";
+ }
+ }
+
+ private async Task AcquireSingleFrameAsync()
+ {
+ try
+ {
+ StatusMessage = "正在采集... | Acquiring...";
+ DetectorResult result = await _detectorService.AcquireSingleFrameAsync();
+
+ if (result.IsSuccess)
+ {
+ StatusMessage = "采集完成 | Acquisition completed";
+ }
+ else
+ {
+ StatusMessage = $"采集失败: {result.ErrorMessage}";
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "单帧采集异常");
+ StatusMessage = $"采集异常: {ex.Message}";
+ }
+ }
+
+ private async Task StartAcquisitionAsync()
+ {
+ try
+ {
+ StatusMessage = "启动连续采集... | Starting continuous acquisition...";
+ DetectorResult result = await _detectorService.StartAcquisitionAsync();
+
+ if (result.IsSuccess)
+ {
+ IsAcquiring = true;
+ DetectorStatus = _detectorService.Status;
+ StatusMessage = "连续采集中... | Continuous acquisition...";
+ }
+ else
+ {
+ StatusMessage = $"启动失败: {result.ErrorMessage}";
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "启动采集异常");
+ StatusMessage = $"启动异常: {ex.Message}";
+ }
+ }
+
+ private async Task StopAcquisitionAsync()
+ {
+ try
+ {
+ StatusMessage = "正在停止... | Stopping...";
+ DetectorResult result = await _detectorService.StopAcquisitionAsync();
+
+ if (result.IsSuccess)
+ {
+ IsAcquiring = false;
+ DetectorStatus = _detectorService.Status;
+ StatusMessage = "已停止 | Stopped";
+ }
+ else
+ {
+ StatusMessage = $"停止失败: {result.ErrorMessage}";
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "停止采集异常");
+ StatusMessage = $"停止异常: {ex.Message}";
+ }
+ }
+
+ private async Task AutoCorrectionAsync()
+ {
+ try
+ {
+ StatusMessage = "正在校正... | Correcting...";
+ DetectorResult result = await _detectorService.AutoCorrectionAsync(10);
+
+ if (result.IsSuccess)
+ {
+ StatusMessage = "校正完成 | Correction completed";
+ }
+ else
+ {
+ StatusMessage = $"校正失败: {result.ErrorMessage}";
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "自动校正异常");
+ StatusMessage = $"校正异常: {ex.Message}";
+ }
+ }
+
+ private void GetInfo()
+ {
+ try
+ {
+ DetectorInfo info = _detectorService.GetInfo();
+ if (info != null)
+ {
+ DetectorModel = info.Model;
+ Resolution = $"{info.MaxWidth}x{info.MaxHeight}";
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "获取信息异常");
+ }
+ }
+
+ #endregion
+
+ #region 命令条件 | Command Conditions
+
+ private bool CanAcquire() => DetectorStatus == DetectorStatus.Ready;
+ private bool CanStartAcquisition() => DetectorStatus == DetectorStatus.Ready && !IsAcquiring;
+ private bool CanStopAcquisition() => IsAcquiring;
+ private bool CanCorrect() => DetectorStatus == DetectorStatus.Ready;
+
+ #endregion
+
+ #region 事件处理 | Event Handlers
+
+ private void SubscribeEvents()
+ {
+ _eventAggregator.GetEvent()
+ .Subscribe(OnImageCaptured, ThreadOption.UIThread);
+
+ _eventAggregator.GetEvent()
+ .Subscribe(OnCorrectionCompleted, ThreadOption.UIThread);
+ }
+
+ private void OnImageCaptured(ImageCapturedEventArgs args)
+ {
+ FrameNumber = args.FrameNumber;
+ }
+
+ private void OnCorrectionCompleted(CorrectionCompletedEventArgs args)
+ {
+ if (args.IsSuccess)
+ {
+ StatusMessage = $"{args.CorrectionType} 校正完成";
+ }
+ }
+
+ #endregion
+ }
+}
+```
+
+---
+## 8. 在应用退出时释放资源 | Release Resources on Application Exit
+
+在 `App.xaml.cs` 的 `OnExit` 方法中添加:
+
+```csharp
+protected override void OnExit(ExitEventArgs e)
+{
+ // 记录应用退出日志 | Log application exit
+ Log.Information("XplorePlane主应用退出(XP.App-OnExit)");
+
+ // 释放探测器资源 | Release detector resources
+ try
+ {
+ var detectorService = Container.Resolve();
+ // 探测器服务会在 Dispose 时自动停止采集和释放资源
+ // Detector service will automatically stop acquisition and release resources on Dispose
+ Log.Information("探测器资源已成功释放 | Detector resources released successfully");
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "探测器资源释放失败,忽略该错误继续退出 | Failed to release detector resources");
+ }
+
+ // 释放其他资源... | Release other resources...
+
+ // 释放Serilog资源(避免日志丢失)| Release Serilog resources
+ Log.CloseAndFlush();
+
+ base.OnExit(e);
+}
+```
+
+---
+
+## 9. 探测器状态 | Detector Status
+
+### 9.1 状态枚举
+
+```csharp
+public enum DetectorStatus
+{
+ Uninitialized = 0, // 未初始化
+ Initializing = 1, // 初始化中
+ Ready = 2, // 就绪
+ Acquiring = 3, // 采集中
+ Correcting = 4, // 校正中
+ Error = 5 // 错误
+}
+```
+
+### 9.2 状态监控
+
+```csharp
+// 监控探测器状态变化 | Monitor detector status changes
+private void MonitorStatus()
+{
+ DetectorStatus currentStatus = _detectorService.Status;
+
+ switch (currentStatus)
+ {
+ case DetectorStatus.Uninitialized:
+ StatusMessage = "未初始化 | Uninitialized";
+ break;
+ case DetectorStatus.Initializing:
+ StatusMessage = "初始化中... | Initializing...";
+ break;
+ case DetectorStatus.Ready:
+ StatusMessage = "就绪 | Ready";
+ break;
+ case DetectorStatus.Acquiring:
+ StatusMessage = "采集中... | Acquiring...";
+ break;
+ case DetectorStatus.Correcting:
+ StatusMessage = "校正中... | Correcting...";
+ break;
+ case DetectorStatus.Error:
+ StatusMessage = "错误 | Error";
+ // 获取错误信息 | Get error information
+ DetectorResult error = _detectorService.GetLastError();
+ _logger.Error(null, $"探测器错误: {error.ErrorMessage}");
+ break;
+ }
+}
+```
+
+---
+
+## 10. 异常处理 | Exception Handling
+
+### 10.1 DetectorResult 结果处理
+
+所有操作返回 `DetectorResult` 对象:
+
+```csharp
+DetectorResult result = await _detectorService.InitializeAsync();
+
+if (result.IsSuccess)
+{
+ // 操作成功 | Operation successful
+ _logger.Info("操作成功");
+}
+else
+{
+ // 操作失败 | Operation failed
+ string error = result.ErrorMessage;
+ int errorCode = result.ErrorCode;
+ Exception exception = result.Exception;
+
+ _logger.Error(exception, $"操作失败: {error}, 错误码: {errorCode}");
+}
+```
+
+### 10.2 异常捕获
+
+```csharp
+try
+{
+ DetectorResult result = await _detectorService.AcquireSingleFrameAsync();
+ // 处理结果 | Process result
+}
+catch (TimeoutException ex)
+{
+ // 超时异常 | Timeout exception
+ _logger.Error(ex, "操作超时");
+ MessageBox.Show("操作超时,请检查探测器连接", "超时",
+ MessageBoxButton.OK, MessageBoxImage.Warning);
+}
+catch (InvalidOperationException ex)
+{
+ // 无效操作异常 | Invalid operation exception
+ _logger.Error(ex, "无效操作");
+ MessageBox.Show($"无效操作: {ex.Message}", "错误",
+ MessageBoxButton.OK, MessageBoxImage.Error);
+}
+catch (Exception ex)
+{
+ // 其他异常 | Other exceptions
+ _logger.Error(ex, "未知错误");
+ MessageBox.Show($"发生错误: {ex.Message}", "错误",
+ MessageBoxButton.OK, MessageBoxImage.Error);
+}
+```
+
+---
+
+## 11. 日志记录 | Logging
+
+模块使用 Serilog 进行结构化日志记录:
+
+### 11.1 日志级别
+
+- **Debug**: 详细的调试信息(帧采集、状态变化等)
+- **Info**: 一般信息(初始化成功、采集开始等)
+- **Warn**: 警告信息(状态异常、参数警告等)
+- **Error**: 错误信息(操作失败、通讯异常等)
+- **Fatal**: 致命错误(系统崩溃等)
+
+### 11.2 日志示例
+
+```csharp
+// 信息日志 | Info log
+_logger.Info("探测器初始化成功 | Detector initialized successfully");
+
+// 警告日志 | Warning log
+_logger.Warn("探测器未就绪 | Detector not ready");
+
+// 错误日志 | Error log
+_logger.Error(ex, $"操作失败: {ex.Message} | Operation failed: {ex.Message}");
+
+// 调试日志 | Debug log
+_logger.Debug($"采集帧 {frameNumber} | Acquired frame {frameNumber}");
+```
+
+---
+
+## 12. 性能优化建议 | Performance Optimization Suggestions
+
+### 12.1 图像处理
+
+避免在 UI 线程处理大图像:
+
+```csharp
+// 推荐:在后台线程处理图像 | Recommended: Process images in background thread
+await Task.Run(() =>
+{
+ // 图像处理逻辑 | Image processing logic
+ ProcessImage(imageData);
+});
+```
+
+### 12.2 异步操作
+
+所有 I/O 操作使用异步方法:
+
+```csharp
+// 正确:异步操作 | Correct: Async operation
+await _detectorService.AcquireSingleFrameAsync();
+
+// 错误:同步阻塞 | Wrong: Synchronous blocking
+_detectorService.AcquireSingleFrameAsync().Wait(); // 不要这样做!
+```
+
+### 12.3 事件订阅
+
+及时取消不再需要的事件订阅:
+
+```csharp
+// 保存订阅令牌 | Save subscription token
+private SubscriptionToken _imageCapturedToken;
+
+// 订阅事件 | Subscribe to event
+_imageCapturedToken = _eventAggregator.GetEvent()
+ .Subscribe(OnImageCaptured, ThreadOption.UIThread);
+
+// 取消订阅 | Unsubscribe
+_eventAggregator.GetEvent()
+ .Unsubscribe(_imageCapturedToken);
+```
+
+---
+
+## 13. 故障排查 | Troubleshooting
+
+### 13.1 初始化失败
+
+**问题**: 探测器初始化失败
+
+**可能原因**:
+- 探测器 IP 地址或端口配置错误
+- 探测器未启动或网络不通
+- 探测器驱动未正确安装
+- 配置文件参数错误
+
+**解决方案**:
+1. 检查 `App.config` 中的探测器连接参数
+2. 使用 `ping` 命令测试探测器网络连接
+3. 确认探测器驱动和 SDK 已安装
+4. 查看日志文件获取详细错误信息
+
+### 13.2 采集失败
+
+**问题**: 无法采集图像
+
+**可能原因**:
+- 探测器未初始化
+- 探测器处于错误状态
+- 校正未完成
+- 网络通讯异常
+
+**解决方案**:
+1. 确认已调用 `InitializeAsync()` 并成功
+2. 检查探测器状态是否为 `Ready`
+3. 执行自动校正 `AutoCorrectionAsync()`
+4. 检查网络连接和通讯日志
+
+### 13.3 校正失败
+
+**问题**: 校正操作失败
+
+**可能原因**:
+- 探测器未就绪
+- 射线源未开启(增益校正需要)
+- 采集帧数不足
+- 环境光干扰
+
+**解决方案**:
+1. 确认探测器状态为 `Ready`
+2. 增益校正前确保射线源已开启
+3. 增加采集帧数(建议 10 帧以上)
+4. 检查暗场环境,避免光线干扰
+
+### 13.4 通讯超时
+
+**问题**: 操作超时
+
+**可能原因**:
+- 网络延迟过高
+- 探测器响应慢
+- 超时配置过短
+
+**解决方案**:
+1. 检查网络连接质量
+2. 增加配置文件中的超时时间
+3. 检查探测器负载情况
+
+---
+
+## 14. 注意事项 | Notes
+
+1. **线程安全**: `IDetectorService` 是单例,多个 ViewModel 共享同一个实例
+2. **异步操作**: 所有操作都是异步的,避免阻塞 UI 线程
+3. **状态检查**: 操作前检查探测器状态,确保处于正确状态
+4. **资源释放**: 应用退出时探测器会自动停止采集并释放资源
+5. **事件订阅**: 记得在 ViewModel 销毁时取消事件订阅
+6. **校正顺序**: 建议按照暗场→增益→坏像素的顺序进行校正
+7. **日志记录**: 所有关键操作都有详细的日志记录
+8. **错误处理**: 始终检查 `DetectorResult.IsSuccess` 并处理错误
+
+---
+
+## 15. 文档索引 | Documentation Index
+
+- **[GUIDENCE.md](./GUIDENCE.md)** - 本文档,详细使用指南
+- **[README.md](./README.md)** - 项目概述和快速参考
+- **[App.config.example](./App.config.example)** - 配置文件示例
+
+---
+
+## 16. 技术支持 | Technical Support
+
+如有问题或建议,请联系项目维护团队。
+
+---
+
+**最后更新 | Last Updated**: 2026-03-11
diff --git a/XP.Hardware.Detector/Documents/README.md b/XP.Hardware.Detector/Documents/README.md
new file mode 100644
index 0000000..ae9b369
--- /dev/null
+++ b/XP.Hardware.Detector/Documents/README.md
@@ -0,0 +1,356 @@
+# XP.Hardware.Detector
+
+工业探测器控制模块 | Industrial Detector Control Module
+
+---
+
+## 项目概述 | Project Overview
+
+XP.Hardware.Detector 是 XplorePlane X 射线检测系统的探测器控制模块,负责与工业 X 射线探测器进行通讯和控制。该模块采用策略模式和工厂模式设计,支持多种探测器型号的统一管理,提供完整的图像采集和校正功能。
+
+### 主要特性 | Key Features
+
+- 支持多种探测器型号(Varex、iRay、Hamamatsu 等)
+- 完整的探测器生命周期管理(初始化、采集、校正)
+- 单帧和连续采集模式
+- 自动校正功能(暗场、增益、坏像素)
+- 实时状态监控
+- 基于 Prism 事件聚合器的松耦合跨模块通讯
+- 异步操作,不阻塞 UI 线程
+- 完整的日志记录和异常处理
+
+---
+
+## 框架架构 | Architecture
+
+```
+XP.Hardware.Detector/
+├── Abstractions/ # 抽象层 | Abstraction Layer
+│ ├── IAreaDetector.cs # 面阵探测器接口
+│ ├── AreaDetectorBase.cs # 抽象基类
+│ ├── DetectorResult.cs # 结果封装类
+│ ├── DetectorInfo.cs # 探测器信息
+│ ├── Enums/ # 枚举定义
+│ │ ├── DetectorStatus.cs
+│ │ └── DetectorType.cs
+│ └── Events/ # Prism 事件定义
+│ ├── ImageCapturedEvent.cs
+│ └── CorrectionCompletedEvent.cs
+├── Implementations/ # 实现层 | Implementation Layer
+│ ├── VarexDetector.cs # Varex 探测器实现
+│ └── IRayDetector.cs # iRay 探测器实现
+├── Factories/ # 工厂层 | Factory Layer
+│ ├── IDetectorFactory.cs # 工厂接口
+│ └── DetectorFactory.cs # 探测器工厂
+├── Services/ # 业务服务层 | Service Layer
+│ ├── IDetectorService.cs # 服务接口
+│ └── DetectorService.cs # 服务实现(单例)
+├── Config/ # 配置层 | Configuration Layer
+│ ├── DetectorConfig.cs # 配置实体
+│ ├── VarexDetectorConfig.cs
+│ ├── IRayDetectorConfig.cs
+│ └── ConfigLoader.cs # 配置加载器
+├── Module/ # Prism 模块 | Prism Module
+│ └── DetectorModule.cs # 模块注册
+├── ViewModels/ # 视图模型 | View Models
+└── Views/ # WPF 视图 | WPF Views
+```
+
+### 设计模式 | Design Patterns
+
+- **策略模式**:`IAreaDetector` 接口定义统一操作,支持多种设备实现
+- **工厂模式**:`IDetectorFactory` 根据设备类型动态创建实例
+- **模板方法模式**:`AreaDetectorBase` 提供基础实现框架
+- **单例模式**:`IDetectorService` 作为全局单例管理设备状态
+- **依赖注入**:通过 Prism 容器管理服务生命周期
+- **事件聚合器**:使用 Prism `IEventAggregator` 实现松耦合通讯
+- **异步模式**:所有 I/O 操作采用 async/await
+
+---
+
+## 核心功能 | Core Features
+
+### 1. 探测器初始化 | Detector Initialization
+
+```csharp
+// 初始化探测器
+DetectorResult result = await _detectorService.InitializeAsync();
+
+if (result.IsSuccess)
+{
+ // 获取探测器信息
+ DetectorInfo info = _detectorService.GetInfo();
+ Console.WriteLine($"型号: {info.Model}, 分辨率: {info.MaxWidth}x{info.MaxHeight}");
+}
+```
+
+### 2. 图像采集 | Image Acquisition
+
+```csharp
+// 单帧采集
+await _detectorService.AcquireSingleFrameAsync();
+
+// 启动连续采集
+await _detectorService.StartAcquisitionAsync();
+
+// 停止采集
+await _detectorService.StopAcquisitionAsync();
+```
+
+### 3. 自动校正 | Auto Correction
+
+```csharp
+// 执行自动校正(暗场+增益+坏像素)
+await _detectorService.AutoCorrectionAsync(frameCount: 10);
+```
+
+### 4. 状态监控 | Status Monitoring
+
+```csharp
+// 检查探测器状态
+DetectorStatus status = _detectorService.Status;
+
+// 获取错误信息
+DetectorResult error = _detectorService.GetLastError();
+```
+
+### 5. 事件通讯 | Event Communication
+
+```csharp
+// 订阅图像采集事件
+_eventAggregator.GetEvent()
+ .Subscribe(OnImageCaptured, ThreadOption.UIThread);
+
+// 订阅校正完成事件
+_eventAggregator.GetEvent()
+ .Subscribe(OnCorrectionCompleted, ThreadOption.UIThread);
+```
+
+---
+
+## 技术要求 | Technical Requirements
+
+### 运行环境 | Runtime Environment
+
+- **.NET 8.0** (net8.0-windows7.0)
+- **Windows 操作系统**(WPF 依赖)
+- **Visual Studio 2022** 或更高版本
+
+### 核心依赖 | Core Dependencies
+
+| 依赖库 | 版本 | 用途 |
+|--------|------|------|
+| **Prism.Wpf** | 9.0.537 | MVVM 框架和依赖注入 |
+| **Serilog** | - | 结构化日志记录 |
+| **System.Configuration.ConfigurationManager** | - | 配置文件管理 |
+| **探测器 SDK** | - | 厂商提供的探测器驱动库 |
+
+### 硬件要求 | Hardware Requirements
+
+- 支持的探测器型号:
+ - Varex(当前支持)
+ - iRay(预留)
+ - Hamamatsu(预留)
+- 网络连接:TCP/IP 或 USB
+- 探测器驱动和 SDK
+
+---
+
+## 快速开始 | Quick Start
+
+### 1. 配置文件设置
+
+在 `App.config` 中添加:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### 2. 注册模块
+
+在 `App.xaml.cs` 中:
+
+```csharp
+protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
+{
+ moduleCatalog.AddModule();
+}
+```
+
+### 3. 使用服务
+
+在 ViewModel 中注入并使用:
+
+```csharp
+public class YourViewModel : BindableBase
+{
+ private readonly IDetectorService _detectorService;
+ private readonly IEventAggregator _eventAggregator;
+
+ public YourViewModel(
+ IDetectorService detectorService,
+ IEventAggregator eventAggregator)
+ {
+ _detectorService = detectorService;
+ _eventAggregator = eventAggregator;
+
+ SubscribeEvents();
+ }
+
+ public async Task InitializeAsync()
+ {
+ DetectorResult result = await _detectorService.InitializeAsync();
+ if (result.IsSuccess)
+ {
+ // 初始化成功
+ }
+ }
+}
+```
+
+---
+
+## 配置参数说明 | Configuration Parameters
+
+| 参数 | 类型 | 默认值 | 说明 |
+|------|------|--------|------|
+| Type | string | Varex | 探测器类型 |
+| IP | string | 192.168.1.200 | 探测器 IP 地址 |
+| Port | int | 50000 | 探测器端口号 |
+| SavePath | string | D:\Images | 图像存储路径 |
+| AutoSave | bool | true | 是否自动保存图像 |
+
+---
+
+## 探测器状态 | Detector Status
+
+```csharp
+public enum DetectorStatus
+{
+ Uninitialized = 0, // 未初始化
+ Initializing = 1, // 初始化中
+ Ready = 2, // 就绪
+ Acquiring = 3, // 采集中
+ Correcting = 4, // 校正中
+ Error = 5 // 错误
+}
+```
+
+---
+
+## 异常处理 | Exception Handling
+
+所有操作返回 `DetectorResult` 对象:
+
+```csharp
+DetectorResult result = await _detectorService.AcquireSingleFrameAsync();
+
+if (result.IsSuccess)
+{
+ // 操作成功
+}
+else
+{
+ // 操作失败
+ string error = result.ErrorMessage;
+ int errorCode = result.ErrorCode;
+ Exception exception = result.Exception;
+
+ _logger.Error(exception, $"操作失败: {error}, 错误码: {errorCode}");
+}
+```
+
+---
+
+## 日志记录 | Logging
+
+模块使用 Serilog 进行结构化日志记录:
+
+- **Debug**:详细的调试信息(帧采集、状态变化等)
+- **Info**:一般信息(初始化成功、采集开始等)
+- **Warn**:警告信息(状态异常、参数警告等)
+- **Error**:错误信息(操作失败、通讯异常等)
+- **Fatal**:致命错误(系统崩溃等)
+
+---
+
+## 资源释放 | Resource Disposal
+
+在应用退出时探测器会自动停止采集并释放资源:
+
+```csharp
+protected override void OnExit(ExitEventArgs e)
+{
+ var detectorService = Container.Resolve();
+ // 服务会自动释放资源
+
+ base.OnExit(e);
+}
+```
+
+---
+
+## 扩展性 | Extensibility
+
+### 添加新的探测器设备
+
+1. 在 `Implementations/` 创建新类继承 `AreaDetectorBase`
+2. 实现所有抽象方法
+3. 在 `DetectorFactory.CreateDetector()` 添加新类型分支
+4. 在配置文件中添加设备配置
+
+```csharp
+public class NewDetector : AreaDetectorBase
+{
+ public override async Task InitializeAsync(CancellationToken cancellationToken = default)
+ {
+ // 实现初始化逻辑
+ }
+
+ // 实现其他抽象方法...
+}
+```
+
+---
+
+## 文档索引 | Documentation Index
+
+- **[GUIDENCE.md](./GUIDENCE.md)** - 详细使用指南,包含完整代码示例
+- **[README.md](./README.md)** - 本文档,项目概述和快速参考
+- **[App.config.example](./App.config.example)** - 配置文件示例
+
+---
+
+## 故障排查 | Troubleshooting
+
+### 初始化失败
+- 检查探测器 IP 地址和端口配置
+- 确认探测器网络连接正常
+- 验证探测器驱动已正确安装
+
+### 采集失败
+- 确认已成功初始化
+- 检查探测器状态是否为 `Ready`
+- 执行自动校正
+
+### 校正失败
+- 确认探测器状态为 `Ready`
+- 增益校正前确保射线源已开启
+- 增加采集帧数(建议 10 帧以上)
+
+详细故障排查请参考 [GUIDENCE.md](./GUIDENCE.md) 第 13 节。
+
+---
+
+**最后更新 | Last Updated**: 2026-03-11
diff --git a/XP.Hardware.Detector/Factories/DetectorFactory.cs b/XP.Hardware.Detector/Factories/DetectorFactory.cs
new file mode 100644
index 0000000..8711cde
--- /dev/null
+++ b/XP.Hardware.Detector/Factories/DetectorFactory.cs
@@ -0,0 +1,119 @@
+using System;
+using System.Collections.Generic;
+using Prism.Events;
+using Prism.Ioc;
+using XP.Hardware.Detector.Abstractions;
+using XP.Hardware.Detector.Abstractions.Enums;
+using XP.Hardware.Detector.Config;
+using XP.Hardware.Detector.Implementations;
+using XP.Common.Logging.Interfaces;
+
+namespace XP.Hardware.Detector.Factories
+{
+ ///
+ /// 探测器工厂实现 | Detector factory implementation
+ /// 配置驱动的探测器实例创建
+ ///
+ public class DetectorFactory : IDetectorFactory
+ {
+ private readonly IEventAggregator _eventAggregator;
+ private readonly IContainerProvider _containerProvider;
+ private readonly ILoggerService _logger;
+
+ ///
+ /// 构造函数 | Constructor
+ ///
+ /// 事件聚合器 | Event aggregator
+ /// Prism 容器 | Prism container
+ /// 日志服务 | Logger service
+ public DetectorFactory(IEventAggregator eventAggregator, IContainerProvider containerProvider, ILoggerService logger = null)
+ {
+ _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
+ _containerProvider = containerProvider ?? throw new ArgumentNullException(nameof(containerProvider));
+ _logger = logger?.ForModule("DetectorFactory");
+ }
+
+ ///
+ /// 创建探测器实例 | Create detector instance
+ ///
+ public DetectorResult CreateDetector(DetectorConfig config)
+ {
+ if (config == null)
+ {
+ var errorMsg = "配置不能为空 | Configuration cannot be null";
+ _logger?.Error(null, errorMsg);
+ return DetectorResult.Failure(errorMsg);
+ }
+
+ try
+ {
+ _logger?.Info($"开始创建探测器,类型:{config.Type} | Starting to create detector, type: {config.Type}");
+
+ IAreaDetector detector = config.Type switch
+ {
+ DetectorType.Varex => CreateVarexDetector(config),
+ //DetectorType.IRay => CreateIRayDetector(config),
+ DetectorType.Hamamatsu => throw new NotImplementedException("Hamamatsu 探测器尚未实现 | Hamamatsu detector not implemented yet"),
+ _ => throw new NotSupportedException($"不支持的探测器类型:{config.Type} | Unsupported detector type: {config.Type}")
+ };
+
+ // 注册到容器(通过 IContainerExtension 注册实例)| Register to container
+ var containerExtension = _containerProvider.Resolve();
+ containerExtension.RegisterInstance(detector);
+ _logger?.Debug("探测器实例已注册到容器 | Detector instance registered to container");
+
+ _logger?.Info($"探测器创建成功,类型:{config.Type} | Detector created successfully, type: {config.Type}");
+ return DetectorResult.Success(detector, "探测器创建成功 | Detector created successfully");
+ }
+ catch (NotImplementedException ex)
+ {
+ var errorMsg = $"探测器类型尚未实现 | Detector type not implemented: {ex.Message}";
+ _logger?.Warn(errorMsg);
+ return DetectorResult