diff --git a/yy-admin-master/YY.Admin.Services/Service/Print/NativePrintRenderService.cs b/yy-admin-master/YY.Admin.Services/Service/Print/NativePrintRenderService.cs index 7e232da..62dc0af 100644 --- a/yy-admin-master/YY.Admin.Services/Service/Print/NativePrintRenderService.cs +++ b/yy-admin-master/YY.Admin.Services/Service/Print/NativePrintRenderService.cs @@ -392,56 +392,67 @@ public static class NativePrintRenderService private static string BuildFreeTableInnerHtml(JsonNode el, JsonObject data, double wMm, double hMm) { - var rowCount = el["rowCount"]?.GetValue() ?? 1; - var colCount = el["colCount"]?.GetValue() ?? 1; + var rowCount = Math.Max(1, el["rowCount"]?.GetValue() ?? 1); + var colCount = Math.Max(1, el["colCount"]?.GetValue() ?? 1); var cells = el["cells"]?.AsArray() ?? []; - var borderColor = ReadAsString(el["borderColor"], "#222222"); - var borderWidth = el["borderWidth"]?.GetValue() ?? 1; - var colWidths = el["colWidths"]?.AsArray()?.Select(n => n?.GetValue() ?? wMm / colCount).ToArray() - ?? Enumerable.Repeat(wMm / colCount, colCount).ToArray(); - var rowHeights = el["rowHeights"]?.AsArray()?.Select(n => n?.GetValue() ?? hMm / rowCount).ToArray() - ?? Enumerable.Repeat(hMm / rowCount, rowCount).ToArray(); + var borderColor = ReadAsString(el["borderColor"], "#222222") ?? "#222222"; + var borderWidth = Math.Max(1, el["borderWidth"]?.GetValue() ?? 1); - var cellMap = new Dictionary<(int row, int col), JsonNode>(); - foreach (var c in cells) - if (c != null) - cellMap[(c["row"]?.GetValue() ?? 0, c["col"]?.GetValue() ?? 0)] = c; + var colWidths = ResolveFreeTableColWidths(el, colCount, wMm); + var rowHeights = ResolveFreeTableRowHeights(el, rowCount, hMm); + var anchors = NormalizeFreeTableAnchors(rowCount, colCount, cells); + + // Pre-group anchors by row for O(n) iteration + var anchorsByRow = anchors + .GroupBy(a => a.Row) + .ToDictionary(g => g.Key, g => g.OrderBy(a => a.Col).ToList()); - var occupied = new HashSet<(int, int)>(); var sb = new StringBuilder(); - sb.Append($""); + sb.Append($"
"); sb.Append(""); - foreach (var cw in colWidths) sb.Append($""); + foreach (var cw in colWidths) + sb.Append($""); sb.Append(""); for (var r = 0; r < rowCount; r++) { var rh = r < rowHeights.Length ? rowHeights[r] : hMm / rowCount; - sb.Append($""); - for (var c = 0; c < colCount; c++) + sb.Append($""); + + if (!anchorsByRow.TryGetValue(r, out var rowAnchors)) + rowAnchors = new List(); + + foreach (var cell in rowAnchors) { - if (occupied.Contains((r, c))) continue; - cellMap.TryGetValue((r, c), out var cell); - var rs = cell?["rowspan"]?.GetValue() ?? 1; - var cs = cell?["colspan"]?.GetValue() ?? 1; - var bindField = ReadAsString(cell?["bindField"]); - var rawText = ReadAsString(cell?["text"], string.Empty); - var cellText = bindField != null - ? ResolveField(data, bindField)?.ToString() ?? rawText - : rawText; - var ta = ReadAsString(cell?["align"], "left"); - var va = ReadAsString(cell?["verticalAlign"], "middle"); - var cFs = cell?["fontSize"]?.GetValue() ?? 12; - var cColor = ReadAsString(cell?["color"], "#111111"); - var cBg = ReadAsString(cell?["backgroundColor"], "#ffffff"); - for (var dr = 0; dr < rs; dr++) - for (var dc = 0; dc < cs; dc++) - if (dr > 0 || dc > 0) occupied.Add((r + dr, c + dc)); + var rs = cell.Rowspan; + var cs = cell.Colspan; + + // 边框计算(完整 outer/inner/hideBorder 体系) + var sides = ResolveFreeTableCellBorderSides(el, anchors, cell, rowCount, colCount); + var lineKeys = ResolveFreeTableCellLineStyleKeys(el, cell.Row, cell.Col, rs, cs, rowCount, colCount); + var borderCss = BorderSidesToCssFragment(sides, borderWidth, borderColor, lineKeys); + + // 跨列宽度 + var spanW = 0d; + for (var ci = cell.Col; ci < cell.Col + cs && ci < colWidths.Length; ci++) + spanW += colWidths[ci]; + + // 内容渲染(支持所有 contentType) + var innerHtml = RenderFreeTableCellContent(cell, data); + + // 排版样式 + var nowrap = !cell.AutoWrap; + var ws = nowrap ? "nowrap" : "normal"; + var wb = nowrap ? "normal" : "break-all"; + var ow = nowrap ? "normal" : "anywhere"; + var lh = nowrap ? $"{rh.ToString("0.###", CultureInfo.InvariantCulture)}mm" : "1.3"; + var widthCss = $"width:{spanW.ToString("0.###", CultureInfo.InvariantCulture)}mm;"; + var rsAttr = rs > 1 ? $" rowspan=\"{rs}\"" : string.Empty; var csAttr = cs > 1 ? $" colspan=\"{cs}\"" : string.Empty; - var bdr = $"border:{borderWidth}px solid {borderColor};"; - sb.Append($""); - sb.Append(EscapeHtml(cellText ?? string.Empty)); + + sb.Append($""); + sb.Append(innerHtml); sb.Append(""); } sb.Append(""); @@ -450,6 +461,376 @@ public static class NativePrintRenderService return sb.ToString(); } + // ─── 自由表格:锚点单元格 ───────────────────────────────────────────── + + private sealed record FreeTableAnchorCell + { + public int Row { get; init; } + public int Col { get; init; } + public int Rowspan { get; init; } = 1; + public int Colspan { get; init; } = 1; + public string Text { get; init; } = string.Empty; + public string? BindField { get; init; } + public string ContentType { get; init; } = "text"; + public string Align { get; init; } = "left"; + public string VerticalAlign { get; init; } = "middle"; + public double FontSize { get; init; } = 12d; + public string Color { get; init; } = "#111111"; + public string BackgroundColor{ get; init; } = "#ffffff"; + public bool HideBorderTop { get; init; } + public bool HideBorderRight { get; init; } + public bool HideBorderBottom { get; init; } + public bool HideBorderLeft { get; init; } + public bool? FillCell { get; init; } // null → true for media content + public double ContentScale { get; init; } = 100d; + public string? ImageFit { get; init; } + public int DecimalPlaces { get; init; } = 2; + public string? AmountType { get; init; } + public bool AutoWrap { get; init; } = true; + } + + private static FreeTableAnchorCell ParseFreeTableCell(JsonObject c) + { + var ct = (ReadAsString(c["contentType"]) ?? "text").Trim().ToLowerInvariant(); + if (ct is not ("text" or "image" or "qrcode" or "barcode" or "number" or "amount")) + ct = "text"; + var bf = ReadAsString(c["bindField"])?.Trim(); + var imgFit = ReadAsString(c["imageFit"]); + if (imgFit is not ("fill" or "contain" or "cover")) imgFit = null; + var amtType = ReadAsString(c["amountType"]); + if (amtType is not ("CNY" or "USD" or "EUR")) amtType = null; + return new FreeTableAnchorCell + { + Row = Math.Max(0, c["row"]?.GetValue() ?? 0), + Col = Math.Max(0, c["col"]?.GetValue() ?? 0), + Rowspan = Math.Max(1, c["rowspan"]?.GetValue() ?? 1), + Colspan = Math.Max(1, c["colspan"]?.GetValue() ?? 1), + Text = ReadAsString(c["text"]) ?? string.Empty, + BindField = string.IsNullOrWhiteSpace(bf) ? null : bf, + ContentType = ct, + Align = ReadAsString(c["align"], "left") ?? "left", + VerticalAlign = ReadAsString(c["verticalAlign"], "middle") ?? "middle", + FontSize = Math.Max(8d, c["fontSize"]?.GetValue() ?? 12d), + Color = ReadAsString(c["color"], "#111111") ?? "#111111", + BackgroundColor = ReadAsString(c["backgroundColor"], "#ffffff") ?? "#ffffff", + HideBorderTop = ReadBoolDefault(c["hideBorderTop"], false), + HideBorderRight = ReadBoolDefault(c["hideBorderRight"], false), + HideBorderBottom = ReadBoolDefault(c["hideBorderBottom"], false), + HideBorderLeft = ReadBoolDefault(c["hideBorderLeft"], false), + FillCell = c["fillCell"] != null ? (bool?)ReadBoolDefault(c["fillCell"], true) : null, + ContentScale = Math.Max(10d, Math.Min(100d, c["contentScale"]?.GetValue() ?? 100d)), + ImageFit = imgFit, + DecimalPlaces = Math.Clamp(c["decimalPlaces"]?.GetValue() ?? 2, 0, 6), + AmountType = amtType, + AutoWrap = !string.Equals(ReadAsString(c["autoWrap"]), "false", StringComparison.OrdinalIgnoreCase), + }; + } + + private static FreeTableAnchorCell DefaultFreeTableCell(int row, int col) => new() + { + Row = row, Col = col + }; + + /// + /// 移植自前端 normalizeFreeTableAnchors:去除重叠、填补空格,返回排序后的锚点列表。 + /// + private static List NormalizeFreeTableAnchors(int rowCount, int colCount, JsonArray cells) + { + var occ = new bool[rowCount, colCount]; + var anchors = new List(); + + // 解析 + 排序 + var parsed = cells.OfType() + .Select(ParseFreeTableCell) + .OrderBy(c => c.Row).ThenBy(c => c.Col) + .ToList(); + + foreach (var c in parsed) + { + var rs = Math.Min(c.Rowspan, rowCount - c.Row); + var cs = Math.Min(c.Colspan, colCount - c.Col); + if (rs < 1 || cs < 1 || c.Row >= rowCount || c.Col >= colCount) continue; + + var overlap = false; + for (var dr = 0; dr < rs && !overlap; dr++) + for (var dc = 0; dc < cs && !overlap; dc++) + if (occ[c.Row + dr, c.Col + dc]) overlap = true; + if (overlap) continue; + + for (var dr = 0; dr < rs; dr++) + for (var dc = 0; dc < cs; dc++) + occ[c.Row + dr, c.Col + dc] = true; + + anchors.Add(c with { Rowspan = rs, Colspan = cs }); + } + + // 填补未覆盖格 + for (var r = 0; r < rowCount; r++) + for (var c = 0; c < colCount; c++) + if (!occ[r, c]) + { + occ[r, c] = true; + anchors.Add(DefaultFreeTableCell(r, c)); + } + + anchors.Sort((a, b) => a.Row != b.Row ? a.Row.CompareTo(b.Row) : a.Col.CompareTo(b.Col)); + return anchors; + } + + /// + /// 找到"拥有" (r, c) 位置的锚点格(考虑合并跨度)。 + /// + private static FreeTableAnchorCell GetFreeTableOwnerAt(List anchors, int r, int c) + { + foreach (var cell in anchors) + { + if (r >= cell.Row && r < cell.Row + Math.Max(1, cell.Rowspan) && + c >= cell.Col && c < cell.Col + Math.Max(1, cell.Colspan)) + return cell; + } + return DefaultFreeTableCell(r, c); + } + + // ─── 自由表格:列宽/行高轨道 ────────────────────────────────────────── + + private static double[] ResolveFreeTableColWidths(JsonNode el, int colCount, double wMm) + { + var raw = el["colWidths"]?.AsArray(); + if (raw == null || raw.Count != colCount) + return EvenSplitTracks(wMm, colCount); + var tracks = raw.Select(n => Math.Max(4d, n?.GetValue() ?? 4d)).ToArray(); + return ClampTrackSumToTotal(tracks, wMm); + } + + private static double[] ResolveFreeTableRowHeights(JsonNode el, int rowCount, double hMm) + { + var raw = el["rowHeights"]?.AsArray(); + if (raw == null || raw.Count != rowCount) + return EvenSplitTracks(hMm, rowCount); + var tracks = raw.Select(n => Math.Max(4d, n?.GetValue() ?? 4d)).ToArray(); + return ClampTrackSumToTotal(tracks, hMm); + } + + private static double[] EvenSplitTracks(double total, int count) + { + var n = Math.Max(1, count); + var t = Math.Max(0.01, total); + var baseVal = Ft2(t / n); + var arr = Enumerable.Repeat(baseVal, n).ToArray(); + var sum = arr.Sum(); + arr[n - 1] = Ft2(arr[n - 1] + (t - sum)); + return arr; + } + + private static double[] ClampTrackSumToTotal(double[] tracks, double total, double minMm = 4d) + { + var n = tracks.Length; + if (n == 0) return Array.Empty(); + var t = Math.Max(0.01, total); + var next = tracks.Select(x => Ft2(Math.Max(minMm, x))).ToArray(); + var sum = next.Sum(); + if (Math.Abs(sum - t) < 0.02) return next; + var scale = t / sum; + next = next.Select(x => Ft2(x * scale)).ToArray(); + sum = next.Sum(); + next[n - 1] = Ft2(next[n - 1] + (t - sum)); + return next; + } + + // round to 2 decimal places(与前端 round2 一致) + private static double Ft2(double x) => Math.Round(x * 100d) / 100d; + + // ─── 自由表格:边框体系 ─────────────────────────────────────────────── + + /// + /// 移植自前端 resolveFreeTableCellBorderSides:计算单元格四边是否画线。 + /// 同时考虑 outerBorder / innerBorder 开关、单元格 hideBorder* 及相邻格共享边。 + /// + private static (bool top, bool right, bool bottom, bool left) ResolveFreeTableCellBorderSides( + JsonNode el, List anchors, + FreeTableAnchorCell cell, int rowCount, int colCount) + { + var ob = el["outerBorder"]; + var ib = el["innerBorder"]; + + // outer 四边缺省均为 true + var outerTop = ReadBoolDefault(ob?["top"], true); + var outerRight = ReadBoolDefault(ob?["right"], true); + var outerBottom = ReadBoolDefault(ob?["bottom"], true); + var outerLeft = ReadBoolDefault(ob?["left"], true); + // inner 缺省横纵均显示 + var innerH = ReadBoolDefault(ib?["horizontal"], true); + var innerV = ReadBoolDefault(ib?["vertical"], true); + + int r = cell.Row, c = cell.Col, rs = cell.Rowspan, cs = cell.Colspan; + int rEnd = r + rs - 1, cEnd = c + cs - 1; + + var top = r == 0 ? outerTop : innerH; + var right = cEnd == colCount - 1 ? outerRight : innerV; + var bottom = rEnd == rowCount - 1 ? outerBottom : innerH; + var left = c == 0 ? outerLeft : innerV; + + // 单元格级别隐藏 + if (cell.HideBorderTop) top = false; + if (cell.HideBorderRight) right = false; + if (cell.HideBorderBottom) bottom = false; + if (cell.HideBorderLeft) left = false; + + // 共享边:相邻格若声明隐藏其接触边,则本格也不画 + if (top && r > 0) + { + for (var cc = c; cc <= cEnd; cc++) + { + if (GetFreeTableOwnerAt(anchors, r - 1, cc).HideBorderBottom) { top = false; break; } + } + } + if (bottom && rEnd < rowCount - 1) + { + var belowRow = r + rs; + for (var cc = c; cc <= cEnd; cc++) + { + if (GetFreeTableOwnerAt(anchors, belowRow, cc).HideBorderTop) { bottom = false; break; } + } + } + if (left && c > 0) + { + for (var rr = r; rr <= rEnd; rr++) + { + if (GetFreeTableOwnerAt(anchors, rr, c - 1).HideBorderRight) { left = false; break; } + } + } + if (right && cEnd < colCount - 1) + { + var rightCol = c + cs; + for (var rr = r; rr <= rEnd; rr++) + { + if (GetFreeTableOwnerAt(anchors, rr, rightCol).HideBorderLeft) { right = false; break; } + } + } + + return (top, right, bottom, left); + } + + /// + /// 移植自前端 resolveFreeTableCellLineStyleKeys:按位置判断各边线型(外框/横/竖)。 + /// + private static (string top, string right, string bottom, string left) ResolveFreeTableCellLineStyleKeys( + JsonNode el, int anchorRow, int anchorCol, int rs, int cs, int rowCount, int colCount) + { + int rEnd = anchorRow + rs - 1, cEnd = anchorCol + cs - 1; + var outerStyle = ReadAsString(el["outerBorderLineStyle"], "solid") ?? "solid"; + var hStyle = ReadAsString(el["innerBorderHorizontalLineStyle"], "solid") ?? "solid"; + var vStyle = ReadAsString(el["innerBorderVerticalLineStyle"], "solid") ?? "solid"; + return ( + top: anchorRow == 0 ? outerStyle : hStyle, + right: cEnd == colCount - 1 ? outerStyle : vStyle, + bottom: rEnd == rowCount - 1 ? outerStyle : hStyle, + left: anchorCol == 0 ? outerStyle : vStyle + ); + } + + private static string BorderSidesToCssFragment( + (bool top, bool right, bool bottom, bool left) sides, + int bw, string color, + (string top, string right, string bottom, string left) lineStyles) + { + var t = sides.top ? $"{bw}px {LineStyleToCss(lineStyles.top)} {color}" : "none"; + var r = sides.right ? $"{bw}px {LineStyleToCss(lineStyles.right)} {color}" : "none"; + var b = sides.bottom ? $"{bw}px {LineStyleToCss(lineStyles.bottom)} {color}" : "none"; + var l = sides.left ? $"{bw}px {LineStyleToCss(lineStyles.left)} {color}" : "none"; + return $"border-top:{t};border-right:{r};border-bottom:{b};border-left:{l};"; + } + + private static string LineStyleToCss(string key) => key switch + { + "dashed" => "dashed", + "dotted" => "dotted", + "dash_dot" => "dashed", + "double_dash_dot" => "double", + _ => "solid" + }; + + // ─── 自由表格:单元格内容渲染(支持所有 contentType)──────────────── + + private static string RenderFreeTableCellContent(FreeTableAnchorCell cell, JsonObject data) + { + var ct = (cell.ContentType ?? "text").ToLowerInvariant(); + + // 解析原始值 + string rawValue; + if (!string.IsNullOrWhiteSpace(cell.BindField)) + rawValue = ResolveField(data, cell.BindField!)?.ToString() ?? cell.Text; + else + rawValue = cell.Text; + + // 数值格式化 + var displayValue = ct is "number" or "amount" + ? FormatFreeTableCellValue(rawValue, cell) + : rawValue; + + // image/qrcode/barcode 使用原始值(URL、扫描数据);文本类使用 displayValue + var innerArg = ct is "image" or "qrcode" or "barcode" ? rawValue : displayValue; + + var fillCell = cell.FillCell != false; // null → true + var scale = Math.Max(10d, Math.Min(100d, cell.ContentScale > 0 ? cell.ContentScale : 100d)); + + if (ct == "image") + { + var fit = cell.ImageFit ?? "contain"; + var ws = fillCell ? "100%" : $"{scale.ToString("0", CultureInfo.InvariantCulture)}%"; + return $""; + } + if (ct == "qrcode") + { + if (string.IsNullOrWhiteSpace(innerArg)) return string.Empty; + try + { + using var gen = new QRCodeGenerator(); + var qrData = gen.CreateQrCode(innerArg, QRCodeGenerator.ECCLevel.M); + using var code = new PngByteQRCode(qrData); + var b64 = Convert.ToBase64String(code.GetGraphic(6)); + var ws = fillCell ? "100%" : $"{scale.ToString("0", CultureInfo.InvariantCulture)}%"; + return $""; + } + catch { return EscapeHtml(innerArg); } + } + if (ct == "barcode") + { + var ws = fillCell ? "100%" : $"{scale.ToString("0", CultureInfo.InvariantCulture)}%"; + var hs = fillCell ? "100%" : $"{Math.Max(20d, scale * 0.6d).ToString("0", CultureInfo.InvariantCulture)}%"; + return $"
{EscapeHtml(innerArg)}
"; + } + return EscapeHtml(displayValue); + } + + private static string FormatFreeTableCellValue(string raw, FreeTableAnchorCell cell) + { + if (!double.TryParse(raw, NumberStyles.Any, CultureInfo.InvariantCulture, out var d) && + !double.TryParse(raw, NumberStyles.Any, CultureInfo.CurrentCulture, out d)) + return raw; + var decimals = Math.Clamp(cell.DecimalPlaces, 0, 6); + var formatted = d.ToString("N" + decimals, CultureInfo.CurrentCulture); + if (string.Equals(cell.ContentType, "amount", StringComparison.OrdinalIgnoreCase)) + { + var symbol = cell.AmountType switch { "USD" => "$", "EUR" => "EUR ", _ => "¥" }; + return symbol + formatted; + } + return formatted; + } + + private static bool ReadBoolDefault(JsonNode? node, bool defaultValue) + { + if (node == null) return defaultValue; + if (node is JsonValue v) + { + if (v.TryGetValue(out var b)) return b; + var s = v.ToString(); + if (string.Equals(s, "false", StringComparison.OrdinalIgnoreCase)) return false; + if (string.Equals(s, "true", StringComparison.OrdinalIgnoreCase)) return true; + } + return defaultValue; + } + // ─── Table / DetailTable ─────────────────────────────────────────────── private static string RenderDetailTable(JsonNode el, JsonObject data, string posStyle, @@ -459,11 +840,17 @@ public static class NativePrintRenderService var source = ReadAsString(el["source"], "mainTable"); var showHeader = !string.Equals(ReadAsString(el["showHeader"], "true"), "false", StringComparison.OrdinalIgnoreCase); var rowHeightMm = Math.Max(6d, el["rowHeight"]?.GetValue() ?? 8d); - var headerBg = ReadAsString(el["headerBgColor"], "#f5f5f5"); + var headerBg = ReadAsString(el["headerBgColor"], "#f5f5f5"); var headerText = ReadAsString(el["headerTextColor"], "#111111"); var columns = el["columns"]?.AsArray() ?? []; if (columns.Count == 0) return string.Empty; + // 元素级字体基准 + var bodyFontSizeBase = el["bodyFontSize"]?.GetValue() ?? 12d; + var headerFontSizeBase = el["headerFontSize"]?.GetValue() ?? 12d; + + var columnsList = columns.OfType().ToList(); + var rows = ResolveTableRows(data, source); var footerMode = (ReadAsString(el["footerTotalMode"], "overall") ?? "overall").ToLowerInvariant(); var showFooter = !string.Equals(ReadAsString(el["footerShowTotal"], "true"), "false", StringComparison.OrdinalIgnoreCase); @@ -475,7 +862,7 @@ public static class NativePrintRenderService .ToArray() ?? []; var strictGrouping = !string.Equals(ReadAsString(el["strictGrouping"], "true"), "false", StringComparison.OrdinalIgnoreCase); - var detailH = el["h"]?.GetValue() ?? 8d; + var detailH = el["h"]?.GetValue() ?? 8d; var repeatFreeConstrains = RepeatingFreeTableConstrainsDetail(allElements, designY, detailH); List> chunks; @@ -496,24 +883,17 @@ public static class NativePrintRenderService var baseStyleNoTop = RemoveCssProperty(RemoveCssProperty(posStyle, "height"), "top"); for (var pageIdx = 0; pageIdx < chunks.Count; pageIdx++) { - // 续页起点: - // - repeatFreeConstrains → 沿用 designY(重复自由表格占据了顶部空间) - // - 否则 → 顶排到页面上边距 + 报表头带高度(消除空白) + // 续页起点 double top; if (pageIdx == 0 || repeatFreeConstrains) - { top = designY + pageIdx * pageHeightMm; - } else - { top = pageIdx * pageHeightMm + marginTopMm + (repeatHeaderByPage ? headerBandHeight : 0d); - } var isLastChunk = pageIdx == chunks.Count - 1; var chunkRows = chunks[pageIdx]; - // 计算当前 chunk 的行合并 rowSpanMap(按 chunk 内相对行号) var rowSpanMap = BuildRowSpanMap(chunkRows, columns, mergeColumnKeys, strictGrouping); sb.Append($"
"); @@ -521,18 +901,35 @@ public static class NativePrintRenderService if (showHeader) { - var headerRows = BuildTableHeaderRows(el, columns.OfType().ToList()); + var headerRows = BuildTableHeaderRows(el, columnsList); var headerHeightMm = Math.Max(6d, el["headerHeight"]?.GetValue() ?? 10d); var headerRowHeightMm = headerHeightMm / Math.Max(1, headerRows.Count); sb.Append("
"); foreach (var hrow in headerRows) { sb.Append($""); - foreach (var cell in hrow) + foreach (var hcell in hrow) { - var rs = cell.RowSpan > 1 ? $" rowspan=\"{cell.RowSpan}\"" : string.Empty; - var cs = cell.ColSpan > 1 ? $" colspan=\"{cell.ColSpan}\"" : string.Empty; - sb.Append($"{EscapeHtml(cell.Title)}"); + var colNode = (hcell.Col >= 0 && hcell.Col < columnsList.Count) + ? (JsonNode?)columnsList[hcell.Col] : null; + var hFontFamily = ReadAsString(colNode?["fontFamily"]) ?? "inherit"; + var hFontColor = ReadAsString(colNode?["fontColor"]) ?? headerText; + var hAutoWrap = !string.Equals(ReadAsString(colNode?["autoWrap"]), "false", StringComparison.OrdinalIgnoreCase); + // 合并跨列宽度之和,用于字号自适应 + var spanWidthMm = 0d; + for (var ci = hcell.Col; ci < hcell.Col + hcell.ColSpan && ci < columnsList.Count; ci++) + spanWidthMm += columnsList[ci]["width"]?.GetValue() ?? 30d; + var cellHeightMm = headerRowHeightMm * hcell.RowSpan; + var hFontSize = ResolvePrintAutoFontSize(colNode, hcell.Title, + Math.Max(1d, spanWidthMm), cellHeightMm, headerFontSizeBase); + var hWs = hAutoWrap ? "normal" : "nowrap"; + var hWb = hAutoWrap ? "break-all" : "normal"; + var hOw = hAutoWrap ? "anywhere" : "normal"; + var hLh = hAutoWrap ? "1.3" : $"{cellHeightMm.ToString("0.###", CultureInfo.InvariantCulture)}mm"; + + var rs = hcell.RowSpan > 1 ? $" rowspan=\"{hcell.RowSpan}\"" : string.Empty; + var cs = hcell.ColSpan > 1 ? $" colspan=\"{hcell.ColSpan}\"" : string.Empty; + sb.Append($"{EscapeHtml(hcell.Title)}"); } sb.Append(""); } @@ -549,7 +946,7 @@ public static class NativePrintRenderService var field = ReadAsString(col?["bindField"]) ?? ReadAsString(col?["field"]); var spanKey = $"{rowIdx}_{field ?? string.Empty}"; if (rowSpanMap.TryGetValue(spanKey, out var spanVal) && spanVal == 0) - continue; // 被上方行合并,跳过此格 + continue; var rowspanAttr = (rowSpanMap.TryGetValue(spanKey, out var rsVal) && rsVal > 1) ? $" rowspan=\"{rsVal}\"" @@ -561,10 +958,22 @@ public static class NativePrintRenderService var raw = !string.IsNullOrWhiteSpace(field) ? ResolveField(row, field!) : null; var text = FormatColumnValue(raw, col, contentType); var html = ResolveTableCellInnerHtml(contentType, text); - var ws = autoWrap ? "normal" : "nowrap"; - var wb = autoWrap ? "break-all" : "normal"; - var lhv = autoWrap ? "1.3" : $"{rowHeightMm.ToString("0.###", CultureInfo.InvariantCulture)}mm"; - sb.Append($"{html}"); + + // 字体:fontFamily / fontColor / fontSize(含 autoFitFont 自适应) + var fontFamily = ReadAsString(col?["fontFamily"], "inherit") ?? "inherit"; + var fontColor = ReadAsString(col?["fontColor"], "#111111") ?? "#111111"; + var colWidthMm = Math.Max(1d, col?["width"]?.GetValue() ?? 30d); + var useCustomFs = string.Equals(ReadAsString(col?["useCustomFontSize"]), "true", StringComparison.OrdinalIgnoreCase); + var bodyBaseSize = useCustomFs + ? (col?["fontSize"]?.GetValue() ?? bodyFontSizeBase) + : bodyFontSizeBase; + var fontSize = ResolvePrintAutoFontSize(col, text, colWidthMm, rowHeightMm, bodyBaseSize); + + var ws = autoWrap ? "normal" : "nowrap"; + var wb = autoWrap ? "break-all" : "normal"; + var ow = autoWrap ? "anywhere" : "normal"; + var lhv = autoWrap ? "1.3" : $"{rowHeightMm.ToString("0.###", CultureInfo.InvariantCulture)}mm"; + sb.Append($"{html}"); } sb.Append(""); } @@ -583,10 +992,6 @@ public static class NativePrintRenderService // ─── 行合并 ──────────────────────────────────────────────────────────── - /// - /// 移植自前端 buildRowSpanMap。返回 "{rowIdx}_{field}" → rowspan 的映射; - /// 值为 0 表示该格被上方行合并,渲染时跳过;值 > 1 表示需加 rowspan 属性。 - /// private static Dictionary BuildRowSpanMap( List rows, JsonArray columns, string[] mergeColumnKeys, bool strictGrouping) @@ -688,8 +1093,6 @@ public static class NativePrintRenderService while (i < rows.Count) { var remaining = rows.Count - i; - // repeatFreeConstrains → 续页可用高度同首页(重复自由表格占据了 designY 空间) - // 否则 → 续页顶排,减去报表头带高度(headerBandMm) var avail = pageIdx == 0 ? innerH - designY - headerHeightMm : repeatFreeConstrains @@ -972,6 +1375,27 @@ public static class NativePrintRenderService return raw.ToString() ?? string.Empty; } + // ─── autoFitFont 字号自适应(移植自前端 resolvePrintAutoFontSize)──── + + /// + /// 当列开启 autoFitFont 时,根据列宽/行高/文本长度动态缩减字号,最小 8px。 + /// + private static double ResolvePrintAutoFontSize( + JsonNode? col, string text, double colWidthMm, double rowHeightMm, double baseSize) + { + var @base = Math.Max(8d, baseSize); + if (!string.Equals(ReadAsString(col?["autoFitFont"]), "true", StringComparison.OrdinalIgnoreCase)) + return @base; + const double pxPerMm = 3.7795275591d; + var widthPx = Math.Max(1d, colWidthMm * pxPerMm); + var heightPx = Math.Max(1d, rowHeightMm * pxPerMm); + var textLen = Math.Max(1, text.Length); + var byWidth = widthPx / Math.Max(1d, textLen * 0.62d); + var noWrap = string.Equals(ReadAsString(col?["autoWrap"]), "false", StringComparison.OrdinalIgnoreCase); + var byHeight = noWrap ? heightPx * 0.55d : heightPx * 0.36d; + return Math.Max(8d, Math.Round(Math.Min(@base, Math.Min(byWidth, byHeight)))); + } + // ─── 多级表头构建 ─────────────────────────────────────────────────────── private sealed class HeaderCell @@ -1089,12 +1513,12 @@ public static class NativePrintRenderService } return node switch { - JsonArray a => a, - JsonObject o => o, + JsonArray a => a, + JsonObject o => o, JsonValue v when v.TryGetValue(out var s) => s, JsonValue v when v.TryGetValue(out var d) => d, JsonValue v when v.TryGetValue(out var b) => b, - _ => node?.ToString() + _ => node?.ToString() }; }