""" 积木报表 (JiMu Report) 创建/编辑工具脚本 用法: python jimureport_creator.py --api-base --token --config config.json 格式见下方示例。 支持的操作: - 创建报表 (action='create') - 编辑报表 (action='edit', 需提供 reportId) config.json 示例(创建): { "action": "create", "reportName": "用户数据统计报表", "datasets": [ { "dbCode": "userlist", "dbChName": "用户列表", "dbDynSql": "SELECT username, realname, sex FROM sys_user WHERE del_flag = 0", "isPage": "1" }, { "dbCode": "sexchart", "dbChName": "性别图表", "dbDynSql": "SELECT sex AS name, COUNT(*) AS value FROM sys_user WHERE del_flag = 0 GROUP BY sex", "isPage": "0", "forChart": true } ], "layout": "chart_top", "table": { "datasetCode": "userlist", "title": "用户数据列表", "columns": [ {"field": "username", "title": "用户账号", "width": 120}, {"field": "realname", "title": "姓名", "width": 100}, {"field": "sex", "title": "性别", "width": 80} ] }, "chart": { "datasetCode": "sexchart", "chartType": "pie.doughnut", "title": "按性别统计", "width": "650", "height": "300" } } """ import urllib.request import json import sys import time import random import hashlib import ssl import argparse # 修复 Windows 控制台中文乱码 sys.stdout.reconfigure(encoding='utf-8') sys.stderr.reconfigure(encoding='utf-8') SIGNATURE_SECRET = 'dd05f1c54d63749eda95f9fa6d49v442a' SIGNED_ENDPOINTS = [ '/jmreport/queryFieldBySql', '/jmreport/executeSelectApi', '/jmreport/loadTableData', '/jmreport/testConnection', '/jmreport/download/image', '/jmreport/dictCodeSearch', '/jmreport/getDataSourceByPage', '/jmreport/getDataSourceById', ] # 默认样式列表 DEFAULT_STYLES = [ # 0: 仅边框 {"border": {"bottom": ["thin", "#000"], "top": ["thin", "#000"], "left": ["thin", "#000"], "right": ["thin", "#000"]}}, # 1: 边框+居中 {"border": {"bottom": ["thin", "#000"], "top": ["thin", "#000"], "left": ["thin", "#000"], "right": ["thin", "#000"]}, "align": "center"}, # 2: 边框+居中+垂直居中(数据行) {"border": {"bottom": ["thin", "#000"], "top": ["thin", "#000"], "left": ["thin", "#000"], "right": ["thin", "#000"]}, "align": "center", "valign": "middle"}, # 3: 边框+居中+垂直居中+蓝底(表头无白字) {"border": {"bottom": ["thin", "#000"], "top": ["thin", "#000"], "left": ["thin", "#000"], "right": ["thin", "#000"]}, "align": "center", "valign": "middle", "bgcolor": "#01b0f1"}, # 4: 边框+居中+垂直居中+蓝底白字(表头推荐) {"border": {"bottom": ["thin", "#000"], "top": ["thin", "#000"], "left": ["thin", "#000"], "right": ["thin", "#000"]}, "align": "center", "valign": "middle", "bgcolor": "#01b0f1", "color": "#ffffff"}, # 5: 边框+居中+垂直居中+深蓝底白字加粗(大标题) {"border": {"bottom": ["thin", "#000"], "top": ["thin", "#000"], "left": ["thin", "#000"], "right": ["thin", "#000"]}, "align": "center", "valign": "middle", "bgcolor": "#4472C4", "color": "#ffffff", "font": {"bold": True}}, ] # 默认图表配色 DEFAULT_CHART_COLORS = ["#5470c6", "#ee6666", "#91cc75", "#fac858", "#73c0de", "#3ba272", "#fc8452", "#9a60b4"] # ====== 工具函数 ====== def gen_id(): """生成唯一ID""" return str(int(time.time() * 1000) * 1000000 + random.randint(100000, 999999)) def compute_sign(params_dict): """计算积木报表接口签名""" str_params = {} for k, v in params_dict.items(): if v is None: continue if isinstance(v, bool): str_params[k] = str(v).lower() elif isinstance(v, (int, float)): str_params[k] = str(v) elif isinstance(v, (dict, list)): str_params[k] = json.dumps(v, ensure_ascii=False, separators=(',', ':')) else: str_params[k] = str(v) sorted_params = dict(sorted(str_params.items())) params_json = json.dumps(sorted_params, ensure_ascii=False, separators=(',', ':')) sign_str = params_json + SIGNATURE_SECRET return hashlib.md5(sign_str.encode('utf-8')).hexdigest().upper() def create_ssl_context(): """创建不验证证书的SSL上下文""" ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE return ctx SSL_CTX = create_ssl_context() def api_request(api_base, token, path, data=None, method=None): """发送API请求,自动处理签名""" url = f'{api_base}{path}' headers = { 'X-Access-Token': token, 'Content-Type': 'application/json; charset=UTF-8' } need_sign = any(path.rstrip('/').endswith(ep.rstrip('/')) for ep in SIGNED_ENDPOINTS) if need_sign: sign_params = data if data else {} headers['X-TIMESTAMP'] = str(int(time.time() * 1000)) headers['X-Sign'] = compute_sign(sign_params) if data is not None: json_data = json.dumps(data, ensure_ascii=False).encode('utf-8') req = urllib.request.Request(url, data=json_data, headers=headers, method=method or 'POST') else: req = urllib.request.Request(url, headers=headers, method=method or 'GET') resp = urllib.request.urlopen(req, context=SSL_CTX) return json.loads(resp.read().decode('utf-8')) # ====== 数据集相关 ====== def parse_sql_fields(api_base, token, sql, db_source=''): """解析SQL获取字段列表""" result = api_request(api_base, token, '/jmreport/queryFieldBySql', { "sql": sql, "dbSource": db_source, "type": "0" }) if not result.get('success'): print(f' SQL解析失败: {result.get("message")}') return [], [] field_list = result.get('result', {}).get('fieldList', []) param_list = result.get('result', {}).get('paramList', []) return field_list, param_list def save_dataset(api_base, token, report_id, ds_config, field_list, param_list): """保存数据集,返回数据集ID""" db_data = { "izSharedSource": 0, "jimuReportId": report_id, "dbCode": ds_config['dbCode'], "dbChName": ds_config.get('dbChName', ds_config['dbCode']), "dbType": ds_config.get('dbType', '0'), "dbSource": ds_config.get('dbSource', ''), "jsonData": ds_config.get('jsonData', ''), "apiConvert": ds_config.get('apiConvert', ''), "isList": ds_config.get('isList', '1'), "isPage": ds_config.get('isPage', '1'), "dbDynSql": ds_config.get('dbDynSql', ''), "fieldList": field_list, "paramList": param_list } result = api_request(api_base, token, '/jmreport/saveDb', db_data) if not result.get('success'): print(f' 数据集保存失败: {result.get("message")}') return None return result['result']['id'] # ====== 报表布局构造 ====== def build_cols(columns): """根据列配置构造cols对象""" cols = {"len": 100} for i, col in enumerate(columns): if col.get('width'): cols[str(i + 1)] = {"width": col['width']} return cols def build_table_rows(table_config, start_row=1, title_style=5, header_style=4, data_style=2): """ 构造数据表格的rows、merges。 返回 (rows_dict, merges_list, next_row) """ rows = {} merges = [] columns = table_config.get('columns', []) ds_code = table_config['datasetCode'] col_count = len(columns) current_row = start_row # 标题行 title = table_config.get('title') if title: cells = {} for i in range(col_count): cells[str(i + 1)] = {"text": title if i == 0 else "", "style": title_style} rows[str(current_row)] = {"cells": cells, "height": 40} if col_count > 1: start_col = chr(ord('A') + 1) # B列开始(列索引1对应B) end_col = chr(ord('A') + col_count) # merge 使用 UI 行号(代码行号+1,因为代码row 0 = UI row 1) ui_row = current_row + 1 merges.append(f"{start_col}{ui_row}:{end_col}{ui_row}") current_row += 1 # 表头行 header_cells = {} for i, col in enumerate(columns): header_cells[str(i + 1)] = {"text": col['title'], "style": header_style} rows[str(current_row)] = {"cells": header_cells, "height": 34} current_row += 1 # 数据绑定行 data_cells = {} for i, col in enumerate(columns): data_cells[str(i + 1)] = {"text": f"#{{{ds_code}.{col['field']}}}", "style": data_style} rows[str(current_row)] = {"cells": data_cells} current_row += 1 return rows, merges, current_row def build_chart_rows(chart_config, chart_db_id, start_row=1, col_start=1, col_end=6, row_count=1): """ 构造图表的虚拟单元格rows和chartList。 row_count 默认为1行(与设计器行为一致,图表通过width/height属性控制大小, 虚拟单元格仅作为锚点,不需要覆盖整个图表区域)。 返回 (rows_dict, chart_list, next_row) """ layer_id = "chart_" + gen_id() rows = {} virtual_cell_range = [] for r in range(start_row, start_row + row_count): cells = {} for c in range(col_start, col_end + 1): cells[str(c)] = {"text": " ", "virtual": layer_id} virtual_cell_range.append([r, c]) rows[str(r)] = {"cells": cells} # ECharts 配置 chart_type = chart_config.get('chartType', 'bar.simple') echarts_config = build_echarts_config(chart_type, chart_config) chart_item = { "row": start_row, "col": col_start, "colspan": 0, "rowspan": 0, "width": str(chart_config.get('width', '650')), "height": str(chart_config.get('height', '350')), "config": json.dumps(echarts_config, ensure_ascii=False), "url": "", "extData": { "chartType": chart_type, "dataType": chart_config.get('dataType', 'sql'), "dataId": chart_db_id, "dbCode": chart_config['datasetCode'], "axisX": "name", "axisY": "value", "series": "type", "xText": "", "yText": "", "apiStatus": "1" }, "layer_id": layer_id, "offsetX": 0, "offsetY": 0, "backgroud": {"enabled": False, "color": "#fff", "image": ""}, "virtualCellRange": virtual_cell_range } return rows, [chart_item], start_row + row_count def build_echarts_config(chart_type, chart_config): """根据图表类型构造ECharts配置""" title_text = chart_config.get('title', '') colors = chart_config.get('colors', DEFAULT_CHART_COLORS) if chart_type.startswith('pie'): # 饼图/环形图/玫瑰图 radius = ["40%", "70%"] if 'doughnut' in chart_type else "70%" if 'rose' in chart_type: radius = [20, "70%"] return { "title": {"text": title_text, "left": "center", "textStyle": {"fontSize": 16}}, "tooltip": {"trigger": "item", "formatter": "{b}: {c} ({d}%)"}, "legend": {"orient": "vertical", "left": "left", "top": "middle"}, "series": [{ "type": "pie", "radius": radius, "center": ["55%", "55%"], "avoidLabelOverlap": True, "itemStyle": {"borderRadius": 6, "borderColor": "#fff", "borderWidth": 2}, "label": {"show": True, "formatter": "{b}: {c}"}, "emphasis": {"label": {"show": True, "fontSize": 16, "fontWeight": "bold"}}, "data": [], "roseType": "area" if 'rose' in chart_type else None }], "color": colors } elif chart_type.startswith('bar'): # 柱状图 is_horizontal = 'horizontal' in chart_type return { "title": {"text": title_text, "left": "center"}, "tooltip": {"trigger": "axis"}, "legend": {"bottom": 0}, "xAxis": [{"type": "value" if is_horizontal else "category", "data": []}], "yAxis": [{"type": "category" if is_horizontal else "value", "data": []}], "series": [{"type": "bar", "data": [], "itemStyle": {"color": colors[0]}}], "color": colors } elif chart_type.startswith('line'): # 折线图 smooth = 'smooth' in chart_type area_style = {"opacity": 0.3} if 'area' in chart_type else None return { "title": {"text": title_text, "left": "center"}, "tooltip": {"trigger": "axis"}, "legend": {"bottom": 0}, "xAxis": [{"type": "category", "data": []}], "yAxis": [{"type": "value"}], "series": [{"type": "line", "data": [], "smooth": smooth, "areaStyle": area_style}], "color": colors } elif chart_type.startswith('gauge'): # 仪表盘 return { "title": {"text": title_text, "left": "center"}, "tooltip": {"formatter": "{b}: {c}"}, "series": [{"type": "gauge", "data": [], "detail": {"formatter": "{value}"}}] } elif chart_type.startswith('radar'): # 雷达图 return { "title": {"text": title_text, "left": "center"}, "tooltip": {}, "legend": {"bottom": 0}, "radar": {"indicator": []}, "series": [{"type": "radar", "data": []}], "color": colors } elif chart_type.startswith('funnel'): # 漏斗图 return { "title": {"text": title_text, "left": "center"}, "tooltip": {"trigger": "item", "formatter": "{b}: {c}"}, "legend": {"bottom": 0}, "series": [{"type": "funnel", "data": [], "left": "10%", "width": "80%"}], "color": colors } else: # 通用默认 return { "title": {"text": title_text, "left": "center"}, "tooltip": {}, "series": [{"type": "bar", "data": []}], "color": colors } # ====== 报表保存 ====== def build_base_save_data(report_id, designer_obj, rows, cols, styles, merges, chart_list=None, page_size=None, area=None, data_rect_width=None): """构造报表保存请求体""" return { "designerObj": json.dumps(designer_obj, ensure_ascii=False), "name": "sheet1", "freeze": "A1", "freezeLineColor": "rgb(185, 185, 185)", "rows": rows, "cols": cols, "styles": styles, "merges": merges, "validations": [], "autofilter": {}, "dbexps": [], "dicts": [], "loopBlockList": [], "zonedEditionList": [], "fixedPrintHeadRows": [], "fixedPrintTailRows": [], "hiddenCells": [], "submitHandlers": [], "rpbar": {"show": True, "pageSize": str(page_size) if page_size else "", "btnList": []}, "fillFormToolbar": {"show": True, "btnList": ["save", "subTable_add", "verify", "subTable_del", "print", "close", "first", "prev", "next", "paging", "total", "last", "exportPDF", "exportExcel", "exportWord"]}, "hidden": {"rows": [], "cols": [], "conditions": {"rows": {}, "cols": {}}}, "fillFormInfo": {"layout": {"direction": "horizontal", "width": 200, "height": 45}}, "recordSubTableOrCollection": {"group": [], "record": [], "range": []}, "displayConfig": {}, "printConfig": {"paper": "A4", "width": 210, "height": 297, "definition": 1, "isBackend": False, "marginX": 10, "marginY": 10, "layout": "portrait", "printCallBackUrl": ""}, "querySetting": {"izOpenQueryBar": False, "izDefaultQuery": True}, "queryFormSetting": {"useQueryForm": False, "dbKey": "", "idField": ""}, "area": area if area is not None else False, "chartList": chart_list or [], "background": False, "dataRectWidth": data_rect_width if data_rect_width is not None else 700, "excel_config_id": report_id, "pyGroupEngine": False, "isViewContentHorizontalCenter": False, "fillFormStyle": "default", "sheetId": "default", "sheetName": "默认Sheet", "sheetOrder": "0" } def save_report(api_base, token, save_data): """调用报表保存接口""" result = api_request(api_base, token, '/jmreport/save', save_data) return result # ====== 主流程 ====== def create_report(api_base, token, config): """创建新报表""" report_id = gen_id() report_code = str(int(time.time() * 1000)) report_name = config['reportName'] print(f'\n{"=" * 50}') print(f'创建积木报表: {report_name}') print(f'{"=" * 50}') # Step 1: 创建空报表 designer_obj = { "id": report_id, "code": report_code, "name": report_name, "type": "0", "template": 0, "delFlag": 0, "viewCount": 0, "updateCount": 0, "submitForm": config.get('submitForm', 0), "reportName": report_name } empty_save = build_base_save_data(report_id, designer_obj, {"len": 200}, {"len": 100}, [], []) print('\n[1/4] 创建空报表...') r = save_report(api_base, token, empty_save) print(f' 结果: success={r.get("success")}') if not r.get('success'): print(f' 失败: {r.get("message")}') return None # Step 2: 解析SQL并保存数据集 print('\n[2/4] 解析SQL并保存数据集...') dataset_ids = {} for ds in config.get('datasets', []): db_code = ds['dbCode'] sql = ds.get('dbDynSql', '') db_source = ds.get('dbSource', '') print(f' 解析数据集 [{db_code}]: {sql[:60]}...' if len(sql) > 60 else f' 解析数据集 [{db_code}]: {sql}') field_list, param_list = parse_sql_fields(api_base, token, sql, db_source) if not field_list: print(f' 警告: 数据集 [{db_code}] 字段为空') continue ds_id = save_dataset(api_base, token, report_id, ds, field_list, param_list) if ds_id: dataset_ids[db_code] = ds_id print(f' 数据集 [{db_code}] 保存成功, id={ds_id}') else: print(f' 数据集 [{db_code}] 保存失败') # Step 3: 构造布局 print('\n[3/4] 构造报表布局...') layout = config.get('layout', 'table_only') table_config = config.get('table') chart_config = config.get('chart') all_rows = {"len": 200} all_merges = [] chart_list = [] col_count = len(table_config['columns']) if table_config else 6 if layout == 'chart_top' and chart_config and table_config: # 图表在上,数据表格在下(避免列表展开与图表冲突) chart_db_id = dataset_ids.get(chart_config['datasetCode'], '') chart_rows, chart_list, next_row = build_chart_rows( chart_config, chart_db_id, start_row=1, col_start=1, col_end=col_count ) all_rows.update(chart_rows) # 分隔行 all_rows[str(next_row)] = {"cells": {}, "height": 10} next_row += 1 # 数据表格 table_rows, table_merges, _ = build_table_rows(table_config, start_row=next_row) all_rows.update(table_rows) all_merges.extend(table_merges) print(f' 布局: 图表在上(rows 1-10) + 数据表(rows {next_row}+)') elif layout == 'chart_bottom' and chart_config and table_config: # 数据表格在上,图表在下 # 图表虚拟行必须在数据展开区域之后,否则预览会重叠 table_rows, table_merges, next_row = build_table_rows(table_config, start_row=1) all_rows.update(table_rows) all_merges.extend(table_merges) # data_binding_row = next_row - 1 (数据绑定行) # 数据展开后最多占 pageSize 行,图表需在展开区域之后 page_size = config.get('pageSize', 10) gap = config.get('gap', 1) # 默认1行间距,更紧凑 data_binding_row = next_row - 1 chart_start = data_binding_row + page_size + gap chart_db_id = dataset_ids.get(chart_config['datasetCode'], '') chart_rows, chart_list, chart_end_row = build_chart_rows( chart_config, chart_db_id, start_row=chart_start, col_start=1, col_end=col_count ) all_rows.update(chart_rows) # 添加分页符行(自动触发滚动条计算) # 分页符放在图表下方约3行的位置(确保在数据展开区域之外) # 使用多个空格作为分页符,避免显示"1" pagination_row = chart_start + page_size + 3 all_rows[str(pagination_row)] = {"cells": {"1": {"text": " "}}} # 确保 len 足够大 if pagination_row > all_rows.get("len", 200): all_rows["len"] = pagination_row + 10 print(f' 布局: 数据表(rows 1-{next_row - 1}) + 间距({gap}行) + 图表(row {chart_start}+) + 分页符(row {pagination_row})') print(f' pageSize={page_size}, 图表在数据展开区域之后') elif layout == 'chart_right' and chart_config and table_config: # 数据表格在左,图表在右 table_rows, table_merges, next_row = build_table_rows(table_config, start_row=1) all_rows.update(table_rows) all_merges.extend(table_merges) chart_db_id = dataset_ids.get(chart_config['datasetCode'], '') chart_col_start = col_count + 2 # 留1列间距 chart_col_end = chart_col_start + 5 chart_rows, chart_list, _ = build_chart_rows( chart_config, chart_db_id, start_row=1, col_start=chart_col_start, col_end=chart_col_end ) # 合并chart_rows到all_rows(同行不同列) for row_key, row_val in chart_rows.items(): if row_key in all_rows and row_key != "len": all_rows[row_key]["cells"].update(row_val["cells"]) else: all_rows[row_key] = row_val print(f' 布局: 数据表(cols 1-{col_count}) + 图表(cols {chart_col_start}-{chart_col_end})') elif layout == 'chart_only' and chart_config: # 仅图表 chart_db_id = dataset_ids.get(chart_config['datasetCode'], '') chart_rows, chart_list, _ = build_chart_rows( chart_config, chart_db_id, start_row=1, col_start=1, col_end=6 ) all_rows.update(chart_rows) print(f' 布局: 仅图表(row 1)') elif table_config: # 仅数据表格(默认) table_rows, table_merges, _ = build_table_rows(table_config, start_row=1) all_rows.update(table_rows) all_merges.extend(table_merges) print(f' 布局: 仅数据表') else: print(' 错误: 未配置 table 或 chart') return None # 构造列宽 cols = build_cols(table_config['columns']) if table_config else {"len": 100} # 计算 dataRectWidth(列宽总和) total_width = sum(col.get('width', 100) for col in cols.values() if isinstance(col, dict)) data_rect_width = total_width if total_width > 0 else 700 # 计算 area(内容边界区域) # area.sri/eri = 内容区域的起始/结束行(UI行号,从1开始) # area.sci/eci = 内容区域的起始/结束列 # area.width/height = 内容区域的总像素宽高 title_h = 40 header_h = 34 row_h = 25 # 默认行高 chart_h = int(chart_config.get('height', 350)) if chart_config else 0 if layout == 'chart_bottom' and chart_config and table_config: # 设置 area 为 false,让系统自动计算滚动高度 # 需要在图表底部添加分页符行,系统才能正确计算 area = False elif layout == 'chart_top' and chart_config and table_config: area = { "sri": 1, "sci": 1, "eri": next_row - 1, "eci": col_count, "width": data_rect_width, "height": chart_h + 10 + title_h + header_h + row_h * 2 } elif layout == 'chart_right' and chart_config and table_config: chart_w = int(chart_config.get('width', 650)) area = { "sri": 1, "sci": 1, "eri": next_row - 1, "eci": chart_col_end, "width": data_rect_width + chart_w, "height": title_h + header_h + row_h * 2 } elif layout == 'chart_only' and chart_config: area = { "sri": 1, "sci": 1, "eri": 1, "eci": col_count, "width": data_rect_width, "height": chart_h } else: area = { "sri": 1, "sci": 1, "eri": next_row - 1, "eci": col_count, "width": data_rect_width, "height": title_h + header_h + row_h * 2 } # Step 4: 保存完整报表 print('\n[4/4] 保存报表设计...') designer_obj["updateCount"] = 1 save_data = build_base_save_data( report_id, designer_obj, all_rows, cols, config.get('styles', DEFAULT_STYLES), all_merges, chart_list, page_size=config.get('pageSize'), area=area, data_rect_width=data_rect_width ) r = save_report(api_base, token, save_data) print(f' 结果: success={r.get("success")}') if r.get('success'): print(f'\n{"=" * 50}') print(f'报表创建成功!') print(f' 报表ID: {report_id}') print(f' 报表名称: {report_name}') print(f' 预览地址: {api_base}/jmreport/view/{report_id}') print(f'{"=" * 50}') return report_id else: print(f' 保存失败: {r.get("message")}') return None def edit_report(api_base, token, config): """编辑已有报表""" report_id = config['reportId'] print(f'\n{"=" * 50}') print(f'编辑积木报表: reportId={report_id}') print(f'{"=" * 50}') # 获取现有报表 print('\n[1/3] 获取现有报表...') r = api_request(api_base, token, f'/jmreport/get/{report_id}', method='GET') if not r.get('success'): print(f' 获取失败: {r.get("message")}') return None existing = r['result'] print(f' 报表名称: {existing.get("name")}') # 获取现有数据集 print('\n[2/3] 获取现有数据集...') tree_r = api_request(api_base, token, f'/jmreport/field/tree/{report_id}', method='GET') if tree_r.get('success') and tree_r.get('result'): for ds in tree_r['result']: print(f' 已有数据集: [{ds.get("dbCode")}] {ds.get("dbChName")}') # 添加新数据集 dataset_ids = {} for ds in config.get('addDatasets', []): db_code = ds['dbCode'] sql = ds.get('dbDynSql', '') db_source = ds.get('dbSource', '') print(f' 新增数据集 [{db_code}]...') field_list, param_list = parse_sql_fields(api_base, token, sql, db_source) ds_id = save_dataset(api_base, token, report_id, ds, field_list, param_list) if ds_id: dataset_ids[db_code] = ds_id print(f' 数据集 [{db_code}] 保存成功, id={ds_id}') # 如果需要更新报表设计(jsonStr),在此处理 if config.get('table') or config.get('chart'): print('\n[3/3] 更新报表设计...') # 重新构造完整布局 layout = config.get('layout', 'chart_top') table_config = config.get('table') chart_config = config.get('chart') all_rows = {"len": 200} all_merges = [] chart_list = [] col_count = len(table_config['columns']) if table_config else 6 if layout == 'chart_top' and chart_config and table_config: chart_db_id = dataset_ids.get(chart_config['datasetCode'], chart_config.get('dataId', '')) chart_rows, chart_list, next_row = build_chart_rows( chart_config, chart_db_id, start_row=1, col_start=1, col_end=col_count ) all_rows.update(chart_rows) all_rows[str(next_row)] = {"cells": {}, "height": 10} next_row += 1 table_rows, table_merges, _ = build_table_rows(table_config, start_row=next_row) all_rows.update(table_rows) all_merges.extend(table_merges) cols = build_cols(table_config['columns']) if table_config else {"len": 100} designer_obj = { "id": report_id, "code": existing.get('code', ''), "name": config.get('reportName', existing.get('name', '')), "type": existing.get('type', '0'), "template": existing.get('template', 0), "delFlag": 0, "viewCount": existing.get('viewCount', 0), "updateCount": (existing.get('updateCount') or 0) + 1, "submitForm": existing.get('submitForm', 0), "reportName": config.get('reportName', existing.get('name', '')) } save_data = build_base_save_data( report_id, designer_obj, all_rows, cols, config.get('styles', DEFAULT_STYLES), all_merges, chart_list ) r = save_report(api_base, token, save_data) print(f' 结果: success={r.get("success")}') print(f'\n编辑完成!') print(f' 预览地址: {api_base}/jmreport/view/{report_id}') return report_id def main(): parser = argparse.ArgumentParser(description='积木报表 (JiMu Report) 创建/编辑工具') parser.add_argument('--api-base', required=True, help='JeecgBoot 后端地址') parser.add_argument('--token', required=True, help='X-Access-Token') parser.add_argument('--config', required=True, help='配置文件路径 (JSON)') args = parser.parse_args() with open(args.config, 'r', encoding='utf-8') as f: config = json.load(f) action = config.get('action', 'create') if action == 'create': create_report(args.api_base, args.token, config) elif action == 'edit': edit_report(args.api_base, args.token, config) else: print(f'未知操作类型: {action}') sys.exit(1) if __name__ == '__main__': main()