Skip to content

Commit e4ea26c

Browse files
pythonHuangclaude
andcommitted
feat: 报表列宽所见即所得(预览HTML生成colgroup+table-layout:fixed按cols[].width渲染)+数据行斑马纹(行属性zebra开关,偶数数据行浅灰底,单元格自带背景色优先,HTML+Excel同步)
Co-Authored-By: Claude Code <noreply@anthropic.com>
1 parent 656b25c commit e4ea26c

2 files changed

Lines changed: 61 additions & 14 deletions

File tree

‎Juggle.Application/Services/Impl/ReportExecutionService.cs‎

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -78,17 +78,11 @@ public static void ValidateSql(string sql)
7878
throw new Exception("SQL 包含不允许的操作: UPDATE");
7979
}
8080

81-
/// <summary>渲染报表为HTML</summary>
81+
/// <summary>渲染报表为HTML(数据行自动扩展、列宽/行高所见即所得、支持斑马纹隔行变色)</summary>
8282
public async Task<string> RenderToHtml(string layoutJson, Dictionary<string, object?>? queryParams = null)
8383
{
8484
using var doc = JsonDocument.Parse(layoutJson);
8585
var root = doc.RootElement;
86-
var sb = new StringBuilder();
87-
sb.AppendLine("<!DOCTYPE html><html><head><meta charset='utf-8'><style>");
88-
sb.AppendLine("table{border-collapse:collapse;font-family:'Microsoft YaHei',sans-serif;}");
89-
sb.AppendLine("td{padding:4px 6px;}");
90-
sb.AppendLine("@media print{@page{size:A4;margin:15mm}}</style></head><body>");
91-
9286
var datasets = await ResolveDatasetsAsync(root, queryParams);
9387
return RenderHtmlCore(root, datasets);
9488
}
@@ -133,8 +127,8 @@ private static string RenderHtmlCore(JsonElement root, Dictionary<string, DataTa
133127
{
134128
var sb = new StringBuilder();
135129
sb.AppendLine("<!DOCTYPE html><html><head><meta charset='utf-8'><style>");
136-
sb.AppendLine("table{border-collapse:collapse;font-family:'Microsoft YaHei',sans-serif;}");
137-
sb.AppendLine("td{padding:4px 6px;}");
130+
sb.AppendLine("table{border-collapse:collapse;font-family:'Microsoft YaHei',sans-serif;table-layout:fixed;}");
131+
sb.AppendLine("td{padding:4px 6px;word-wrap:break-word;}");
138132
sb.AppendLine("@media print{@page{size:A4;margin:15mm}}</style></head><body>");
139133

140134
var cellMap = BuildCellMap(root);
@@ -143,6 +137,17 @@ private static string RenderHtmlCore(JsonElement root, Dictionary<string, DataTa
143137
var templateUsed = new HashSet<(int r, int c)>(); // 静态行之间的跨行合并占用
144138

145139
sb.AppendLine("<table>");
140+
// 列宽所见即所得:按设计器 cols[].width 生成 colgroup
141+
if (root.TryGetProperty("cols", out var colsEl) && colsEl.ValueKind == JsonValueKind.Array)
142+
{
143+
sb.Append("<colgroup>");
144+
foreach (var col in colsEl.EnumerateArray())
145+
{
146+
var w = col.TryGetProperty("width", out var we) && we.ValueKind == JsonValueKind.Number ? Math.Max(0, we.GetInt32()) : 0;
147+
sb.Append(w > 0 ? $"<col style='width:{w}px'/>" : "<col/>");
148+
}
149+
sb.AppendLine("</colgroup>");
150+
}
146151
foreach (var rr in materialized)
147152
{
148153
// 应用设计器的行高设置
@@ -164,6 +169,9 @@ private static string RenderHtmlCore(JsonElement root, Dictionary<string, DataTa
164169

165170
var style = hasCell ? "border:1px solid #ccc;" + ReadCellStyle(cell) : "border:1px solid #ccc;";
166171
if (rowHeight > 0) style += $"height:{rowHeight}px;";
172+
// 斑马纹:偶数数据行加浅灰底(单元格自带背景色时优先生效)
173+
if (rr.Zebra && rr.RowNumber % 2 == 0 && (!hasCell || !HasBgColor(cell)))
174+
style += "background-color:#f5f7fa;";
167175
if (colspan > 1) sb.Append($"<td colspan='{colspan}' style='{style}'>");
168176
else sb.Append($"<td style='{style}'>");
169177
sb.Append(value);
@@ -198,11 +206,13 @@ private static List<MaterializedRow> MaterializeRows(JsonElement root, Dictionar
198206

199207
var dt = !string.IsNullOrEmpty(datasetId) && datasets.TryGetValue(datasetId, out var v) ? v : null;
200208

209+
var zebra = rowEl.TryGetProperty("zebra", out var zEl) && zEl.ValueKind == JsonValueKind.True;
210+
201211
if (dt != null && (expand == "auto" || type == "data"))
202212
{
203213
// 数据扩展行:每条数据渲染一行
204214
for (int di = 0; di < dt.Rows.Count; di++)
205-
result.Add(new MaterializedRow { TemplateRow = ri, DataIndex = di, RowNumber = di + 1, Dataset = dt });
215+
result.Add(new MaterializedRow { TemplateRow = ri, DataIndex = di, RowNumber = di + 1, Dataset = dt, Zebra = zebra });
206216
}
207217
else
208218
{
@@ -415,8 +425,15 @@ private class MaterializedRow
415425
public int? DataIndex { get; set; } // 数据集行索引(null=静态行)
416426
public int RowNumber { get; set; } // 数据行序号(1-based,静态行为 0)
417427
public DataTable? Dataset { get; set; } // 本行绑定的数据集
428+
public bool Zebra { get; set; } // 斑马纹:偶数数据行浅灰底
418429
}
419430

431+
/// <summary>单元格是否显式设置了背景色。</summary>
432+
private static bool HasBgColor(JsonElement cell)
433+
=> cell.TryGetProperty("style", out var sEl)
434+
&& sEl.TryGetProperty("bgColor", out var bg)
435+
&& bg.ValueKind == JsonValueKind.String;
436+
420437
/// <summary>导出 Excel (.xlsx) — ClosedXML(与 HTML 同一套扩展/聚合/占位符逻辑)</summary>
421438
public async Task<byte[]> ExportExcelAsync(string layoutJson, Dictionary<string, object?>? queryParams = null)
422439
{
@@ -479,6 +496,10 @@ private static byte[] ExportExcelCore(JsonElement root, Dictionary<string, DataT
479496
xlCell.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
480497
}
481498

499+
// 斑马纹:偶数数据行浅灰底(单元格自带背景色时优先生效)
500+
if (rr.Zebra && rr.RowNumber % 2 == 0 && !HasBgColor(cell))
501+
xlCell.Style.Fill.BackgroundColor = XLColor.FromHtml("#f5f7fa");
502+
482503
for (int dc = 1; dc < colspan; dc++) used.Add(ci + dc);
483504
if (rr.DataIndex == null)
484505
for (int dr = 0; dr < rowspan; dr++)

‎JuggleNet6.Frontend/src/views/report/ReportDesigner.vue‎

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@
159159
</div>
160160
<div v-else-if="rowType==='data'" style="margin-top:6px;color:#909399;font-size:11px;line-height:1.6">
161161
绑定数据集后按<b>每条数据自动扩展一行</b>。单元格可写 <code>${字段名}</code>、<code>${数据集.字段}</code>、<code>${rowIndex}</code>(行号)或 <code>=公式</code>。
162+
<el-checkbox v-model="rowZebra" size="small" @change="updateRowZebra" style="margin-top:4px">斑马纹(预览隔行浅灰底)</el-checkbox>
162163
</div>
163164
<div v-else-if="rowType==='footer'" style="margin-top:6px;color:#909399;font-size:11px;line-height:1.6">
164165
渲染 1 次(数据行下方)。绑定数据集后支持聚合:<code>${金额:SUM}</code>、<code>${字段:AVG}</code>、MIN / MAX / COUNT。
@@ -397,6 +398,8 @@ const cellValue = ref(''), rowType = ref('data')
397398
const rowTypes = ref<string[]>([]) // 每行类型: title/header/data/footer
398399
const rowDatasets = ref<Record<number, string>>({}) // 行号 → 数据集id
399400
const rowDataset = ref('') // 当前选中行绑定的数据集
401+
const rowZebras = ref<Record<number, boolean>>({}) // 行号 → 斑马纹开关
402+
const rowZebra = ref(false) // 当前选中行的斑马纹
400403
const showPreview = ref(false), pageSettingsVisible = ref(false)
401404
const formulaHelpVisible = ref(false)
402405
const previewHtml = ref(''), previewParams = ref<any[]>([])
@@ -413,7 +416,7 @@ const canUndo = computed(() => undoStack.value.length > 0)
413416
const canRedo = computed(() => redoStack.value.length > 0)
414417
415418
function snapState() {
416-
return JSON.stringify({ cells: cells.value, rows: rowHeights.value, types: rowTypes.value.slice(), datasets: { ...rowDatasets.value }, cols: colWidths.value, maxR: maxRows.value, maxC: maxCols.value })
419+
return JSON.stringify({ cells: cells.value, rows: rowHeights.value, types: rowTypes.value.slice(), datasets: { ...rowDatasets.value }, zebras: { ...rowZebras.value }, cols: colWidths.value, maxR: maxRows.value, maxC: maxCols.value })
417420
}
418421
419422
function pushHistory() {
@@ -441,6 +444,7 @@ function restoreState(snap: string) {
441444
rowHeights.value = s.rows || []
442445
rowTypes.value = s.types || []
443446
rowDatasets.value = s.datasets || {}
447+
rowZebras.value = s.zebras || {}
444448
colWidths.value = s.cols || []
445449
maxRows.value = s.maxR || 10
446450
maxCols.value = s.maxC || 6
@@ -630,6 +634,8 @@ function loadLayoutJson() {
630634
rowTypes.value = layout.rows?.map((r:any)=> r.type || 'header') || []
631635
rowDatasets.value = {}
632636
layout.rows?.forEach((r:any, i:number) => { if (r.dataset) rowDatasets.value[i] = r.dataset })
637+
rowZebras.value = {}
638+
layout.rows?.forEach((r:any, i:number) => { if (r.zebra === true) rowZebras.value[i] = true })
633639
colWidths.value = layout.cols?.map((c:any)=>c.width||100) || []
634640
pageSize.value = layout.page?.size||'A4'; pageOrientation.value = layout.page?.orientation||'portrait'
635641
if (layout.page?.margin) Object.assign(pageMargin, layout.page.margin)
@@ -683,6 +689,7 @@ function onCellMouseDown(_e:MouseEvent, r:number, c:number) {
683689
cellValue.value = cell?.value||''
684690
rowType.value = rowTypes.value[r] || (rowHeights.value[r]==='data'?'data':'header')
685691
rowDataset.value = rowDatasets.value[r] || ''
692+
rowZebra.value = !!rowZebras.value[r]
686693
boldActive.value = cell?.style?.bold||false
687694
italicActive.value = cell?.style?.italic||false
688695
selFontSize.value = cell?.style?.fontSize||12
@@ -762,6 +769,11 @@ function updateRowDataset() {
762769
if (rowDataset.value) rowDatasets.value[selR.value] = rowDataset.value
763770
else delete rowDatasets.value[selR.value]
764771
}
772+
function updateRowZebra() {
773+
if (selR.value < 0) return
774+
if (rowZebra.value) rowZebras.value[selR.value] = true
775+
else delete rowZebras.value[selR.value]
776+
}
765777
766778
function applyStyle(prop:string, val?:any) {
767779
pushHistory()
@@ -834,13 +846,19 @@ function insertRow(dir: number) {
834846
cells.value = newCells
835847
rowHeights.value.splice(r, 0, 25)
836848
rowTypes.value.splice(r, 0, 'header')
837-
// 数据集绑定按新行号重映射
849+
// 数据集绑定/斑马纹按新行号重映射
838850
const newDs: Record<number, string> = {}
839851
for (const [k, v] of Object.entries(rowDatasets.value)) {
840852
const idx = Number(k)
841853
newDs[idx >= r ? idx + 1 : idx] = v
842854
}
843855
rowDatasets.value = newDs
856+
const newZb: Record<number, boolean> = {}
857+
for (const [k, v] of Object.entries(rowZebras.value)) {
858+
const idx = Number(k)
859+
newZb[idx >= r ? idx + 1 : idx] = v
860+
}
861+
rowZebras.value = newZb
844862
maxRows.value++
845863
if (dir > 0) selR.value++
846864
}
@@ -859,14 +877,21 @@ function deleteRow() {
859877
cells.value = newCells
860878
rowHeights.value.splice(r, 1)
861879
rowTypes.value.splice(r, 1)
862-
// 数据集绑定按新行号重映射
880+
// 数据集绑定/斑马纹按新行号重映射
863881
const newDs: Record<number, string> = {}
864882
for (const [k, v] of Object.entries(rowDatasets.value)) {
865883
const idx = Number(k)
866884
if (idx > r) newDs[idx - 1] = v
867885
else if (idx < r) newDs[idx] = v
868886
}
869887
rowDatasets.value = newDs
888+
const newZb: Record<number, boolean> = {}
889+
for (const [k, v] of Object.entries(rowZebras.value)) {
890+
const idx = Number(k)
891+
if (idx > r) newZb[idx - 1] = v
892+
else if (idx < r) newZb[idx] = v
893+
}
894+
rowZebras.value = newZb
870895
maxRows.value--
871896
selR.value = Math.min(selR.value, maxRows.value - 1)
872897
}
@@ -912,7 +937,8 @@ function saveLayoutJson() {
912937
height:typeof rowHeights.value[r]==='string'?25:(rowHeights.value[r]||25),
913938
type:rowTypes.value[r] || (rowHeights.value[r]==='data'?'data':'header'),
914939
dataset:rowDatasets.value[r] || '',
915-
expand:(rowTypes.value[r]==='data' && rowDatasets.value[r]) ? 'auto' : ''
940+
expand:(rowTypes.value[r]==='data' && rowDatasets.value[r]) ? 'auto' : '',
941+
zebra:rowZebras.value[r] === true
916942
})
917943
const cols=[]; for (let c=0;c<maxCols.value;c++) cols.push({width:colWidths.value[c]||100})
918944
const cellArr=Object.entries(cells.value).map(([k,v]:any)=>({r:Number(k.split(',')[0]),c:Number(k.split(',')[1]),...v}))

0 commit comments

Comments
 (0)