Skip to content

Commit 53eab57

Browse files
pythonHuangclaude
andcommitted
feat: 报表查询参数增强-新增单选组/筛选组类型+下拉/单选组/筛选组选项支持固定或绑定数据集(值字段/显示字段)+下拉单选/多选/模糊查询+默认值;修复设计器预览与正式访问页不显示查询参数(paramsConfig保存同步+layout.params回退)
Co-Authored-By: Claude Code <noreply@anthropic.com>
1 parent 9e5b30c commit 53eab57

6 files changed

Lines changed: 185 additions & 33 deletions

File tree

‎Juggle.Api/Controllers/Api/ReportController.cs‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,32 @@ public async Task<ApiResult> Info(long id)
8686
{
8787
var rpt = await _db.Set<ReportEntity>().FindAsync(id);
8888
if (rpt == null) return ApiResult.Fail("报表不存在");
89-
return ApiResult.Success(new { rpt.Id, rpt.Name, rpt.GroupName, rpt.ParamsConfig, rpt.Status });
89+
// 参数配置:优先 paramsConfig 列;旧数据未写入时回退到 layoutJson.params
90+
var paramsConfig = rpt.ParamsConfig;
91+
if (string.IsNullOrWhiteSpace(paramsConfig))
92+
{
93+
try
94+
{
95+
using var doc = System.Text.Json.JsonDocument.Parse(rpt.LayoutJson ?? "{}");
96+
var root = doc.RootElement;
97+
if (root.TryGetProperty("params", out var p) && p.ValueKind == System.Text.Json.JsonValueKind.Array)
98+
paramsConfig = p.ToString();
99+
}
100+
catch { /* 忽略解析失败 */ }
101+
}
102+
return ApiResult.Success(new { rpt.Id, rpt.Name, rpt.GroupName, ParamsConfig = paramsConfig, rpt.Status });
103+
}
104+
105+
/// <summary>查询参数选项绑定数据集:执行数据集 SQL 返回行数据(供下拉/单选组/筛选组选项)</summary>
106+
[HttpPost("dataset-options")]
107+
public async Task<ApiResult> DatasetOptions([FromBody] ReportDatasetOptionsRequest req)
108+
{
109+
try
110+
{
111+
var rows = await _reportExec.GetDatasetRowsAsync(req.Id, req.DatasetId);
112+
return ApiResult.Success(rows);
113+
}
114+
catch (Exception ex) { return ApiResult.Fail(ex.Message); }
90115
}
91116

92117
[HttpPost("export-pdf")]
@@ -115,3 +140,9 @@ public class ReportPreviewRequest
115140
public int Page { get; set; } = 1;
116141
public int PageSize { get; set; } = 20;
117142
}
143+
144+
public class ReportDatasetOptionsRequest
145+
{
146+
public long Id { get; set; }
147+
public string? DatasetId { get; set; }
148+
}

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,31 @@ public async Task<string> RenderToHtml(string layoutJson, Dictionary<string, obj
109109
return RenderHtmlCore(root, datasets);
110110
}
111111

112+
/// <summary>
113+
/// 查询参数选项绑定数据集:执行报表内指定数据集的 SQL 返回行数据。
114+
/// (不带查询参数 → @参数名 全部绑 NULL,配合 SQL 判空写法返回全量选项)
115+
/// </summary>
116+
public async Task<List<Dictionary<string, object?>>> GetDatasetRowsAsync(long reportId, string? datasetId)
117+
{
118+
var rpt = await _db.Set<Juggle.Domain.Entities.ReportEntity>().FindAsync(reportId)
119+
?? throw new Exception("报表不存在");
120+
using var doc = JsonDocument.Parse(rpt.LayoutJson ?? "{}");
121+
var root = doc.RootElement;
122+
var datasets = await ResolveDatasetsAsync(root, null);
123+
if (string.IsNullOrEmpty(datasetId) || !datasets.TryGetValue(datasetId, out var dt))
124+
return new List<Dictionary<string, object?>>();
125+
126+
var rows = new List<Dictionary<string, object?>>();
127+
foreach (DataRow row in dt.Rows)
128+
{
129+
var dict = new Dictionary<string, object?>();
130+
foreach (DataColumn col in dt.Columns)
131+
dict[col.ColumnName] = row[col];
132+
rows.Add(dict);
133+
}
134+
return rows;
135+
}
136+
112137
/// <summary>执行 layoutJson 中定义的全部数据集,返回 数据集id → DataTable。</summary>
113138
private async Task<Dictionary<string, DataTable>> ResolveDatasetsAsync(JsonElement root, Dictionary<string, object?>? queryParams)
114139
{

‎JuggleNet6.Frontend/src/components/ReportParams.vue‎

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,72 @@
1010
<el-date-picker v-else-if="p.type==='date'" v-model="values[p.name]" size="small" type="date" value-format="YYYY-MM-DD" style="width:160px" placeholder="选择日期" />
1111
<!-- 开关 -->
1212
<el-switch v-else-if="p.type==='switch'" v-model="values[p.name]" size="small" />
13-
<!-- 下拉 -->
14-
<el-select v-else-if="p.type==='select'" v-model="values[p.name]" size="small" style="width:160px" clearable placeholder="请选择">
15-
<el-option v-for="(o, i) in selectOptions(p)" :key="i" :label="o" :value="o" />
13+
<!-- 下拉(可多选/模糊查询) -->
14+
<el-select v-else-if="p.type==='select'" v-model="values[p.name]" size="small" style="width:170px" clearable
15+
:multiple="!!p.multiple" :filterable="!!p.filterable" :collapse-tags="!!p.multiple" :placeholder="p.multiple ? '可多选' : '请选择'">
16+
<el-option v-for="(o, i) in optionsOf(p)" :key="i" :label="optionLabel(p, o)" :value="optionValue(p, o)" />
1617
</el-select>
18+
<!-- 单选组 -->
19+
<el-radio-group v-else-if="p.type==='radio'" v-model="values[p.name]" size="small">
20+
<el-radio v-for="(o, i) in optionsOf(p)" :key="i" :value="optionValue(p, o)">{{ optionLabel(p, o) }}</el-radio>
21+
</el-radio-group>
22+
<!-- 筛选组(多选) -->
23+
<el-checkbox-group v-else-if="p.type==='checkgroup'" v-model="values[p.name]" size="small">
24+
<el-checkbox v-for="(o, i) in optionsOf(p)" :key="i" :value="optionValue(p, o)">{{ optionLabel(p, o) }}</el-checkbox>
25+
</el-checkbox-group>
1726
</div>
1827
</div>
1928
</template>
2029

2130
<script setup lang="ts">
31+
import { ref, onMounted } from 'vue'
32+
import request from '../utils/request'
33+
import { parseOptions } from '../utils/reportParams'
34+
2235
const props = defineProps<{
23-
params: Array<{ name: string; label?: string; type?: string; options?: string; default?: any }>
36+
params: Array<any>
2437
values: Record<string, any>
38+
/** 报表 ID(选项绑定数据集时用) */
39+
reportId?: number | string
2540
}>()
2641
27-
/** 下拉选项解析:支持逗号或换行分隔 */
28-
function selectOptions(p: any): string[] {
29-
if (Array.isArray(p.options)) return p.options
30-
return String(p.options || '').split(/[,,\n]/).map((s: string) => s.trim()).filter(Boolean)
42+
// 数据集选项缓存:paramName → [{value,label}] 或原始行数组
43+
const datasetRows = ref<Record<string, any[]>>({})
44+
const optionsLoaded = ref<Record<string, boolean>>({})
45+
46+
onMounted(() => {
47+
for (const p of props.params || []) {
48+
if (p.optionsSource === 'dataset' && p.optionsDataset) loadDatasetOptions(p)
49+
}
50+
})
51+
52+
async function loadDatasetOptions(p: any) {
53+
if (!props.reportId) return
54+
const key = `${p.optionsDataset}|${p.optionsValueField}|${p.optionsLabelField}`
55+
if (datasetRows.value[key]) { optionsLoaded.value[p.name] = true; return }
56+
try {
57+
const res: any = await request.post('/report/dataset-options', { id: props.reportId, datasetId: p.optionsDataset })
58+
datasetRows.value[key] = res.data || []
59+
} catch { datasetRows.value[key] = [] }
60+
optionsLoaded.value[p.name] = true
61+
}
62+
63+
/** 选项列表:固定选项或数据集绑定 */
64+
function optionsOf(p: any): any[] {
65+
if (p.optionsSource === 'dataset') {
66+
const key = `${p.optionsDataset}|${p.optionsValueField}|${p.optionsLabelField}`
67+
return datasetRows.value[key] || []
68+
}
69+
return parseOptions(p)
70+
}
71+
72+
function optionValue(p: any, o: any): string {
73+
if (typeof o === 'object') return String(o[p.optionsValueField || 'value'] ?? '')
74+
return String(o)
75+
}
76+
77+
function optionLabel(p: any, o: any): string {
78+
if (typeof o === 'object') return String(o[p.optionsLabelField || 'label'] ?? o[p.optionsValueField || 'value'] ?? '')
79+
return String(o)
3180
}
3281
</script>

‎JuggleNet6.Frontend/src/utils/reportParams.ts‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
/**
2-
* 报表查询参数工具:默认值初始化(URL 参数值优先)与查询载荷构建(空值不传 → 可选过滤)。
2+
* 报表查询参数工具:
3+
* - initParamValues:默认值初始化(URL 查询参数优先)并按类型转换
4+
* - buildParamPayload:查询载荷构建(空值不传;多选/筛选组以逗号拼接)
35
*/
46

57
/** 初始化查询参数值:默认值 → URL 查询参数覆盖 → 按类型转换 */
@@ -12,20 +14,34 @@ export function initParamValues(params: any[], urlQuery?: Record<string, any>):
1214
}
1315
if (p.type === 'switch') v = v === true || v === 'true' || v === '1'
1416
else if (p.type === 'number') v = (v === '' || v === undefined || v === null) ? undefined : Number(v)
17+
else if (p.type === 'checkgroup') {
18+
// 筛选组/多选:支持数组或逗号分隔字符串
19+
v = Array.isArray(v) ? v : String(v ?? '').split(',').map((s: string) => s.trim()).filter(Boolean)
20+
} else if (p.type === 'select' && p.multiple) {
21+
v = Array.isArray(v) ? v : (v ? String(v).split(',').map((s: string) => s.trim()).filter(Boolean) : [])
22+
}
1523
values[p.name] = v ?? ''
1624
}
1725
return values
1826
}
1927

20-
/** 构建查询载荷:空值跳过(后端跳过绑定 → 未填即不过滤) */
28+
/** 构建查询载荷:空值跳过;筛选组/多选以逗号拼接字符串传给 @参数名 */
2129
export function buildParamPayload(params: any[], values: Record<string, any>): Record<string, any> {
2230
const payload: Record<string, any> = {}
2331
for (const p of params || []) {
2432
let v = values[p.name]
2533
if (v === undefined || v === null || v === '') continue
34+
if (Array.isArray(v) && v.length === 0) continue
2635
if (p.type === 'number') v = Number(v)
2736
if (p.type === 'switch') v = v === true
37+
if (Array.isArray(v)) v = v.join(',')
2838
payload[p.name] = v
2939
}
3040
return payload
3141
}
42+
43+
/** 固定选项解析:支持逗号/换行分隔字符串或数组 */
44+
export function parseOptions(p: any): string[] {
45+
if (Array.isArray(p.options)) return p.options
46+
return String(p.options || '').split(/[,,\n]/).map((s: string) => s.trim()).filter(Boolean)
47+
}

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

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -364,27 +364,54 @@ NOW() / TODAY() / ROW() 当前时间 / 日期 / 行号</pre>
364364
</el-dialog>
365365

366366
<!-- 查询参数配置 -->
367-
<el-dialog v-model="paramDialogVisible" title="查询参数配置" width="820px" append-to-body>
368-
<div v-for="(p, i) in previewParams" :key="i" style="display:flex;gap:6px;margin-bottom:6px;align-items:center">
369-
<el-input v-model="p.name" placeholder="参数名(如 beginDate)" size="small" style="width:150px;flex-shrink:0" />
370-
<el-input v-model="p.label" placeholder="显示名" size="small" style="width:110px;flex-shrink:0" />
371-
<el-select v-model="p.type" size="small" style="width:90px;flex-shrink:0">
372-
<el-option value="text" label="文本" />
373-
<el-option value="number" label="数字" />
374-
<el-option value="date" label="日期" />
375-
<el-option value="switch" label="开关" />
376-
<el-option value="select" label="下拉" />
377-
</el-select>
378-
<el-input v-if="p.type==='select'" v-model="p.options" placeholder="选项,逗号/换行分隔" size="small" style="width:180px;flex-shrink:0" />
379-
<el-input v-else-if="p.type!=='switch'" v-model="p.default" placeholder="默认值" size="small" style="width:110px;flex-shrink:0" />
380-
<el-switch v-else v-model="p.default" size="small" />
381-
<span v-if="p.type==='switch'" style="font-size:11px;color:#888;width:70px;flex-shrink:0">{{ p.default ? '默认开' : '默认关' }}</span>
382-
<el-button size="small" type="danger" link @click="previewParams.splice(i, 1)">删</el-button>
367+
<el-dialog v-model="paramDialogVisible" title="查询参数配置" width="960px" append-to-body>
368+
<div v-for="(p, i) in previewParams" :key="i" style="border:1px solid #eee;border-radius:6px;padding:8px;margin-bottom:8px">
369+
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
370+
<el-input v-model="p.name" placeholder="参数名(如 beginDate)" size="small" style="width:150px;flex-shrink:0" />
371+
<el-input v-model="p.label" placeholder="显示名" size="small" style="width:110px;flex-shrink:0" />
372+
<el-select v-model="p.type" size="small" style="width:100px;flex-shrink:0">
373+
<el-option value="text" label="文本" />
374+
<el-option value="number" label="数字" />
375+
<el-option value="date" label="日期" />
376+
<el-option value="switch" label="开关" />
377+
<el-option value="select" label="下拉" />
378+
<el-option value="radio" label="单选组" />
379+
<el-option value="checkgroup" label="筛选组" />
380+
</el-select>
381+
<template v-if="p.type==='switch'">
382+
<el-switch v-model="p.default" size="small" />
383+
<span style="font-size:11px;color:#888">{{ p.default ? '默认开' : '默认关' }}</span>
384+
</template>
385+
<template v-else-if="['select','radio','checkgroup'].includes(p.type)">
386+
<el-select v-model="p.optionsSource" size="small" style="width:90px;flex-shrink:0">
387+
<el-option value="fixed" label="固定选项" />
388+
<el-option value="dataset" label="数据集" />
389+
</el-select>
390+
<template v-if="p.optionsSource !== 'dataset'">
391+
<el-input v-model="p.options" placeholder="选项,逗号/换行分隔" size="small" style="width:220px;flex-shrink:0" />
392+
</template>
393+
<template v-else>
394+
<el-select v-model="p.optionsDataset" placeholder="绑定数据集" size="small" style="width:140px;flex-shrink:0">
395+
<el-option v-for="ds in datasets" :key="ds.id" :label="ds.name" :value="ds.id" />
396+
</el-select>
397+
<el-input v-model="p.optionsValueField" placeholder="值字段" size="small" style="width:90px;flex-shrink:0" />
398+
<el-input v-model="p.optionsLabelField" placeholder="显示字段" size="small" style="width:100px;flex-shrink:0" />
399+
</template>
400+
<template v-if="p.type==='select'">
401+
<el-checkbox v-model="p.multiple" size="small" style="margin-right:0">多选</el-checkbox>
402+
<el-checkbox v-model="p.filterable" size="small" style="margin-right:0">模糊</el-checkbox>
403+
</template>
404+
<el-input v-model="p.default" :placeholder="p.type==='checkgroup' ? '默认值(逗号分隔)' : '默认值'" size="small" style="width:110px;flex-shrink:0" />
405+
</template>
406+
<el-input v-else v-model="p.default" placeholder="默认值" size="small" style="width:110px;flex-shrink:0" />
407+
<el-button size="small" type="danger" link @click="previewParams.splice(i, 1)">删</el-button>
408+
</div>
383409
</div>
384-
<el-button size="small" @click="previewParams.push({ name:'', label:'', type:'text', default:'' })">+添加参数</el-button>
410+
<el-button size="small" @click="previewParams.push({ name:'', label:'', type:'text', default:'', optionsSource:'fixed', options:'', optionsDataset:'', optionsValueField:'', optionsLabelField:'', multiple:false, filterable:false })">+添加参数</el-button>
385411
<div style="margin-top:8px;color:#909399;font-size:12px">
386-
参数类型:文本/数字/日期/开关/下拉(下拉需填选项,逗号或换行分隔)。默认值在正式访问页生效;访问 URL 带同参数名(如 ?beginDate=2026-01-01)时以 URL 值为准。<br>
387-
数据集 SQL 中用 <code>@参数名</code> 引用即可按查询条件过滤,推荐写成 <code>(@参数名 IS NULL OR 字段 = @参数名)</code> / <code>(@kw IS NULL OR 字段 LIKE '%' || @kw || '%')</code> — 未填值时该条件自动失效(不过滤)。
412+
参数类型:文本/数字/日期/开关/下拉/单选组/筛选组。下拉支持单选/多选/模糊查询;下拉、单选组、筛选组的选项可用<b>固定选项</b>(逗号或换行分隔)或<b>绑定数据集</b>(选数据集+值字段+显示字段);筛选组与下拉多选的值以逗号拼接传给 SQL。<br>
413+
默认值在正式访问页生效;访问 URL 带同参数名(如 ?beginDate=2026-01-01)时以 URL 值为准。<br>
414+
数据集 SQL 中用 <code>@参数名</code> 引用即可按查询条件过滤,推荐 <code>(@参数名 IS NULL OR 字段 = @参数名)</code> — 未填值时该条件自动失效(不过滤)。
388415
</div>
389416
<template #footer>
390417
<el-button @click="paramDialogVisible=false">关闭</el-button>
@@ -394,7 +421,7 @@ NOW() / TODAY() / ROW() 当前时间 / 日期 / 行号</pre>
394421
<!-- 预览 -->
395422
<el-dialog v-model="showPreview" title="预览" width="90%" top="5vh">
396423
<div style="display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap;align-items:flex-end">
397-
<ReportParams :params="previewParams" :values="previewValues" />
424+
<ReportParams :params="previewParams" :values="previewValues" :report-id="form.id" />
398425
<el-button size="small" type="primary" @click="doRender">查询</el-button>
399426
<el-button size="small" @click="doPrint">打印</el-button>
400427
</div>
@@ -715,7 +742,9 @@ function loadLayoutJson() {
715742
pageHeader.value = layout.page?.header||''; pageFooter.value = layout.page?.footer||''
716743
pageBgImage.value = layout.page?.bgImage||''
717744
cells.value = {}; if (layout.cells) for (const c of layout.cells) cells.value[`${c.r},${c.c}`] = c
718-
previewParams.value = layout.params||[]
745+
previewParams.value = layout.params && layout.params.length > 0
746+
? layout.params
747+
: (() => { try { return JSON.parse(form.paramsConfig || '[]') } catch { return [] } })()
719748
if (layout.datasets) datasets.value = layout.datasets.map((d:any)=>({...d,expanded:true,loading:false}))
720749
} catch {}
721750
}
@@ -1071,6 +1100,8 @@ function onCellDrop(e:DragEvent, r:number, c:number) { const field = e.dataTrans
10711100
function onGridScroll() {}
10721101
10731102
function saveLayoutJson() {
1103+
// 同步查询参数到 paramsConfig 列(正式访问页读取该字段)
1104+
form.paramsConfig = JSON.stringify(previewParams.value)
10741105
const rows=[]; for (let r=0;r<maxRows.value;r++) rows.push({
10751106
height:typeof rowHeights.value[r]==='string'?25:(rowHeights.value[r]||25),
10761107
type:rowTypes.value[r] || (rowHeights.value[r]==='data'?'data':'header'),

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
<!-- 查询条件 -->
1414
<el-card v-if="params.length>0" style="margin-bottom:12px" shadow="never">
15-
<ReportParams :params="params" :values="values" />
15+
<ReportParams :params="params" :values="values" :report-id="reportId" />
1616
</el-card>
1717

1818
<!-- 报表内容 -->

0 commit comments

Comments
 (0)