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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +