1. 这个需求到底踩中了什么痛点
做过ALV报表的同行都清楚,日常开发的报表几乎都是“列写死”的:数据字典定义结构、字段目录填好、显示出来、完事。但一旦碰上“列数不固定”的需求,这套固定思维立刻失灵。
我最早遇到动态列需求是在一个销售分析项目上。业务方的要求很朴素:同一个报表,今天想看物料组的维度对比,明天想按销售区域展开,后天又要只看某几个月的趋势列。更麻烦的是,列不是简单地显示或隐藏,而是因为筛选维度不同,整张报表的列结构都要跟着变。如果用固定的结构硬做,那得在代码里穷举几十种列组合,维护成本直接爆炸。
这类需求在SAP项目里有个通用叫法——动态列显示,本质上是让ALV的列集合在运行时才确定下来。它通常包含两层含义:第一层是列的内容动态变化,比如按用户勾选的维度决定展示哪些列;第二层是列的数量动态变化,比如按日期范围生成对应的月份列、周列。很多刚接触这个概念的开发者会误以为只要用FIELD-CATALOG控制一下列的显隐就算完事,但真正的动态列场景里,连承载数据的内部表结构本身都需要在运行时构建。
这篇文章我会从技术选型、动态内表构建、字段目录组装、数据填充、常见坑点几个层面展开,重点讲清楚每一步背后的原理,而不是只丢一段能跑的代码。适合正在做报表增强、通用查询工具,或者被复杂报表需求折磨的开发同学参考。
2. 动手之前先理清三条技术路线
2.1 旧版ALV与新版ALV的取舍
SAP里做ALV有两条主流路线:函数方案REUSE_ALV_GRID_DISPLAY和面向对象方案CL_GUI_ALV_GRID。动态列这个需求,两条路线都能实现,但体验差别很大。
函数方案使用SLIS_T_FIELDCAT结构描述字段目录,处理动态列时常见做法是把字段目录结构定义成SLIS_T_FIELDCAT,然后逐个填充,最后传给函数。这个方案的优势是代码量少、写法经典,很多老项目里现成的模板一大把,接手的人看着眼熟。但它有个痛点:单元格编辑、事件处理、自定义工具栏这些功能,用函数方案做起来要绕好几个弯,动态列场景下尤其明显——因为列一旦动态生成,列相关的事件(比如某个动态列的点按、某个动态汇总字段的点击排序)往往需要按列名去判断,函数方案里这些逻辑写起来非常别扭。
CL_GUI_ALV_GRID这边用的是LVC_T_FCAT类型,配合LVC_S_FCAT结构描述每一列。面向对象方案的优势是事件处理干净、单元格编辑直接、前端控制粒度细,而且LVC_S_FCAT本身就支持比较丰富的列属性控制。动态列场景下,我更推荐直接上对象方案,哪怕前期多写几行代码也值得。
2.2 字段目录驱动与动态内表驱动
很多人一听到动态列就想到动态内表,但动态列并不是非得配动态内表。这里需要分清楚两种实现模式。
第一种是字段目录驱动。内表结构固定不变,只是通过字段目录控制哪些列显示、哪些列隐藏,或者给同一列的标题、列宽动态赋值。这种模式适合“列总数已知,只是显隐变化”的场景。比如一张表有20个字段,用户勾选其中10个显示,那不需要动内表结构,只需要在字段目录里控制NO_OUT字段就行。这个方案实现简单、性能有保障,能满足一部分动态列需求。
第二种是动态内表驱动。列结构本身在运行时才确定,内表是临时生成的。这种模式适合“列数、列名完全由参数决定”的场景,比如上面说的按月份动态生成列,内表里根本没有预先定义的字段。数据处理时要么用动态SQL直接查出来拼进动态内表,要么拿到数据后按列名填充到动态内表里。
实际项目里这两个模式经常混合使用:固定字段走静态结构,动态字段走动态内表的附加列。关键是要先想清楚当前需求到底落在哪个模式,别一上来就堆动态内表,后面维护起来容易抽象。
2.3 参数驱动的列定义容器
动态列的“动态”从哪里来?这需要先定义清楚列描述的来源。我一般把动态列的来源分为三种:
- 用户输入驱动:用户通过选择屏幕勾选要显示的列、要展开的维度,程序根据选择屏幕的值决定列集合。
- 主数据驱动:列来自某个配置表。比如报表平台里维护了一套列配置,每个报表模板对应一组列定义,程序运行时读取配置表动态生成列。
- 数据内容驱动:列由查询结果本身决定。比如先执行一次汇总查询,拿到返回结果里有哪几个维度值,每个维度值生成一列,交叉表场景就是这么干的。
这三种来源对应不同的代码组织方式。用户输入驱动最简单,直接读选择屏幕参数拼列即可;主数据驱动需要设计配置表结构,比如列名、列标题、数据类型、数据来源表达式、排序序号等;数据内容驱动最复杂,因为你需要先跑一个“查询列”的步骤,再根据查询结果跑第二遍取数逻辑。
到这里,问题已经清晰了一半。真正的编码核心是两块:动态内表如何构建、字段目录如何与动态内表对齐。下面分别展开。
3. 动态内表的构建原理与实操
3.1 为什么非得动态建表
假设要做一张按月展示销售额的报表,月份范围由用户选择。如果用户选了3个月,表结构就有1个固定列“物料”加3个月份列;如果选了12个月,就有1个固定列加12个月份列。用静态结构怎么做?要么定义成固定12个月的列,多出来的留空,要么定义成几十、上百个预留列再隐藏。这两种做法都有明显的浪费和维护隐患。
动态内表的逻辑就清晰很多:先算出一共有多少列,每个列叫什么名、什么类型、多长,然后运行时构建一个内表,让ALV的字段目录和它完全对应。
3.2 动态类型的核心机制
SAP里创建动态内表有两种经典做法。
第一种是基于类描述创建动态类型,用的比较多的是通过CL_ABAP_TABLEDESCR配合结构类型描述CL_ABAP_STRUCTDESCR来构建。步骤如下:
- 定义一个空的结构类型描述。
- 循环往里面添加组件(列名、类型、长度等)。
- 基于结构类型描述创建表类型,然后
CREATE DATA创建动态数据对象。 - 用
ASSIGN给字段符号分配内表。
第二种相对轻量,用语句直接拼。早期的ABAP支持在DATA声明里使用TYPE TABLE OF配合动态结构,但实际项目里更常见的是用运行时类型创建(RTTI)的方式。
这里分享一段我常用的动态内表生成代码,方便直观理解:
METHOD build_dynamic_table. DATA: lo_struct_type TYPE REF TO cl_abap_structdescr, lo_table_type TYPE REF TO cl_abap_tabledescr, lt_components TYPE cl_abap_structdescr=>component_table, ls_component LIKE LINE OF lt_components. FIELD-SYMBOLS: <fs_table> TYPE STANDARD TABLE. " 添加固定列 CLEAR ls_component. ls_component-name = 'MATNR'. ls_component-type = cl_abap_datadescr=>describe_by_name( 'MATNR' ). APPEND ls_component TO lt_components. " 添加动态列,列名循环生成 LOOP AT mt_dynamic_cols INTO DATA(ls_col). CLEAR ls_component. ls_component-name = ls_col-fieldname. ls_component-type = cl_abap_datadescr=>describe_by_name( ls_col_rollname ). APPEND ls_component TO lt_components. ENDLOOP. " 创建结构类型和表类型 lo_struct_type = cl_abap_structdescr=>create( lt_components ). lo_table_type = cl_abap_tabledescr=>create( p_line_type = lo_struct_type p_table_kind = cl_abap_tabledescr=>tablekind_std ). " 分配内存 CREATE DATA mr_data TYPE HANDLE lo_table_type. ASSIGN mr_data->* TO <fs_table>. ENDMETHOD.这段代码里有几个关键细节需要留意。
组件的name必须和字段目录里的fieldname严格一致,大小写都敏感,拼错一个后面显示出来就是空白列。组件类型直接用describe_by_name传入数据元素或数据字典类型是最省事的做法,能保证类型长度和ALV里引用到的一致。比如MATNR这种类型,直接传入字符'MATNR'即可拿到对应的类型描述。
cl_abap_tabledescr=>create创建表类型时,p_table_kind参数决定内表类型。标准表用tablekind_std,排序表用tablekind_sorted,哈希表用tablekind_hashed。ALV展示一般用标准表就够。
CREATE DATA之后,内存对象是一个数据引用,必须用ASSIGN解引用到字段符号上,否则没法操作。后面往动态内表里写数据,全程都要通过字段符号来做。
3.3 字段符号读写动态列
有了动态内表,怎么往里填数据?这里有个重要的ABAP基本功——动态组件访问。常用的写法有两种。
第一种是用ASSIGN COMPONENT按组件名取列值:
ASSIGN COMPONENT 'MATNR' OF STRUCTURE <fs_struct> TO <fs_field>. IF sy-subrc = 0. <fs_field> = ls_data-matnr. ENDIF.这种方式适合按已知列名逐个赋值,缺点是循环里多次调用ASSIGN COMPONENT,性能上有点损耗,但可读性好、定位清晰。
第二种是放弃字段符号,直接用SET语句(这种用法比较少见),或者更常见的配合动态工作区字段符号循环。实际写数据时,我倾向于先把源数据表读了,再按列映射关系逐列写入。比如:
LOOP AT lt_source INTO DATA(ls_source). ASSIGN <fs_table> TO <fs_line>. ASSIGN COMPONENT 'MATNR' OF STRUCTURE <fs_line> TO <fs_field>. <fs_field> = ls_source-matnr. LOOP AT mt_dynamic_cols INTO DATA(ls_col). ASSIGN COMPONENT ls_col-fieldname OF STRUCTURE <fs_line> TO <fs_field>. IF sy-subrc = 0. <fs_field> = ls_source-(ls_col-source_field). ENDIF. ENDLOOP. APPEND <fs_line> TO <fs_table>. ENDLOOP.注意这里APPEND <fs_line>实际上是把当前工作区的内容追加到内表,而不是追加工作区本身。这个细节新手容易懵,因为字段符号可以当作内表行工作区来用,但APPEND操作对象得是内表字段符号本身。
动态内表还有一种变体玩法:不是把所有列都动态构建,而是用“固定结构+动态扩展”的方式。这种方式在早期ABAP版本里通过EXTEND语句实现,现在不太推荐了,因为动态类型的方式更干净。
4. 字段目录与数据填充的完整链路
4.1 字段目录动态生成的落地方案
动态内表建好了,ALV展示时还需要一份字段目录。字段目录不直接等于内表的组件列表,因为每个列还有很多展示属性要定义,比如列标题、列宽、对齐、合计、是否可编辑、是否隐藏等。
字段目录的生成要跟动态内表的组件列表严格对齐。这里分享我常用的构建逻辑:
METHOD build_field_catalog. DATA: ls_fcat TYPE lvc_s_fcat. " 先取动态内表的组件信息 DATA(lo_struct) = CAST cl_abap_structdescr( cl_abap_tabledescr=>describe_by_data_ref( mr_data ) ). DATA(lt_components) = lo_struct->get_components( ). LOOP AT lt_components INTO DATA(ls_comp). CLEAR ls_fcat. ls_fcat-fieldname = ls_comp-name. ls_fcat-ref_table = 'KONN'. ls_fcat-ref_field = ls_comp-name. ls_fcat-coltext = ls_comp-name. ls_fcat-outputlen = 20. ls_fcat-seltext = ls_comp-name. " 按列名特殊处理 CASE ls_comp-name. WHEN 'MATNR'. ls_fcat-emphasize = 'C300'. ls_fcat-hotspot = 'X'. WHEN 'UMSOK'. ls_fcat-do_sum = 'X'. ls_fcat-no_out = ''. WHEN OTHERS. " 动态列默认不允许编辑 ls_fcat-edit = ''. ENDCASE. APPEND ls_fcat TO mt_fcat. ENDLOOP. ENDMETHOD.这段代码里有几个点值得说明。
第一,ref_table和ref_field可以引用数据字典里的某个字段,这样列的F1帮助、类型定义、货币单位处理都会自动继承,省得人为控制类型信息。不过动态生成的列如果在数据字典里找不到对应字段,就不能硬引用,否则会报错。这种情况下只能手工设置inttype、intlen、decimals等字段。
第二,coltext、seltext、tooltip这些文本属性的优先级,ALV在列宽不够时显示策略有讲究。日常开发中我习惯统一用coltext做列标题,避免重复设置导致显示混乱。
第三,汇总字段要注意。do_sum在ALV里一旦设为X,这列底部网格会自动汇总。如果这列本身是字符串或引用类型,汇总时会直接报错。动态列场景下,建议统一判断列的inttype是否C、N、P类型,是数值类才可以设置汇总。
4.2 数据填充的两种经典策略
字段目录建好,数据也得灌进动态内表。这里实际项目中有两种策略,各有适用场景。
策略一:先查明细,再按列映射填充。适合从多个来源表取数,字段对应关系明确、但需要做大量格式转换的场景。优点是可以单步调试每列的数据来源,缺点是循环里做ASSIGN COMPONENT次数多,大数据量(超过几万行)会明显慢。
策略二:动态SQL直接构建内表。这在数据源本身就是透明表、动态列来自某个汇总维度时非常高效。比如:
SELECT (mt_sel_fields) FROM ztable INTO CORRESPONDING FIELDS OF TABLE <fs_table> WHERE ...注意动态字段列表mt_sel_fields是一个字符串拼出来的,字段顺序要与动态内表组件顺序一致,否则CORRESPONDING FIELDS OF会匹配不上。这种做法的优势是写一次SQL就完成数据填充,劣势是字段列表一旦拼错,SQL编译时报错,排查起来要仔细核对。
从工程角度讲,我建议数据量可控时优先用策略一,因为代码可读性和调试性更好。数据量大到超过十万行时再考虑策略二,并配合适当的动态SQL校验逻辑。
4.3 从数据源到展示层的一体化设计
实操中我会把动态列的整个过程做成一条流水线:
- 根据选择屏幕参数计算列清单,存到
mt_dynamic_cols里(包含列名、类型、标题、来源字段等元数据)。 - 用列清单生成动态内表结构。
- 根据列清单生成字段目录。
- 查询数据源,填充动态内表。
- 调用
CL_GUI_ALV_GRID的SET_TABLE_FOR_FIRST_DISPLAY展示。
这五步的顺序不能乱。字段目录必须在展示前建好;动态内表和字段目录的列名必须完全对齐。
这里有个体验细节:字段目录中列的no_out控制是否输出。如果某列因为数据没取到、或者权限原因不应该展示,可以在字段目录里控制,不用重建内表。这个特性在权限控制场景里非常管用——数据查询统一做,展示列按权限过滤即可。
5. 一个完整的动态销售分析报表案例
5.1 需求与选择屏幕设计
用一个实际案例来串起整条链路。
需求:做一张销售分析报表,用户可以选择分析维度(物料组、销售区域),还可以自由勾选要展示的月份(最多12个月)。报表固定列是“物料号+物料描述”,动态列是按月份生成的销售额列。
选择屏幕设计:
SELECTION-SCREEN BEGIN OF BLOCK sel WITH FRAME TITLE text-t01. PARAMETERS: p_dimen TYPE c length 1 DEFAULT 'G'. " G-物料组, R-区域(FOR SELECTION) SELECT-OPTIONS: s_mon FOR ztable-zmonth OBLIGATORY. SELECTION-SCREEN END OF BLOCK sel.5.2 动态列清单计算
这一步很关键,它会决定后面所有代码。
METHOD calc_dynamic_cols. " 固定两列:物料号、物料描述 " 动态列:按月份生成 DATA: lv_mon_start TYPE i, lv_mon_count TYPE i. " 简单示例:按月生成 LOOP AT s_mon INTO DATA(ls_mon). lv_mon_start = ls_mon-low. EXIT. ENDLOOP. " 根据选择屏幕的月份区间,算出每个月对应一个列名,比如M202401 " 注意:生产环境要处理选择屏幕之间的数量,这里只给思路 DO 12 TIMES. DATA(lv_month) = lv_mon_start + sy-index - 1. DATA(lv_fieldname) = |M{ lv_month WIDTH = 6 ALIGN = RIGHT }|. CONDENSE lv_fieldname NO-GAPS. APPEND INITIAL LINE TO mt_dynamic_cols ASSIGNING FIELD-SYMBOL(<fs_col>). <fs_col>-fieldname = lv_fieldname. <fs_col>-rollname = 'ZMONTHVAL'. <fs_col>-coltext = |{ lv_month }月|. ENDDO. ENDMETHOD.动态列名的生成规则要格外小心。列名必须是合法ABAP字段名,不能以数字开头,所以一般会加前缀。上面代码里我用了M前缀加月份数字,算是最稳妥的做法。实际项目里建议把年也拼进去,比如M202401,避免跨年时列名重复。
另外一个常见的坑是列名长度。ABAP的组件名最长30个字符,这个约束在动态列名里同样适用。像ZCJ001_ZCJ_GROUP_NAME_EXTENDED这种名字本身就接近上限,拼接时容易超出,建议统一定个规范,比如“M+年月”或者“D+日期”。
5.3 数据查询与填充
数据查询策略取决于数据源结构。假设有个事实表ZTP_FACT,存储每天的物料销售记录,字段为物料号、月份、金额。按月份动态扩展列时,SQL没法直接把行转列,需要先按物料号汇总,再把每个月的值填到对应列上。
METHOD fill_data. TYPES: BEGIN OF ty_key, matnr TYPE matnr, maktx TYPE maktx, END OF ty_key. DATA: lt_key TYPE SORTED TABLE OF ty_key WITH UNIQUE KEY matnr, ls_key TYPE ty_key. FIELD-SYMBOLS: <fs_table> TYPE STANDARD TABLE, <fs_line> TYPE data, <fs_field> TYPE any. ASSIGN mr_data->* TO <fs_table>. " 第一步:取物料主数据 SELECT matnr, maktx FROM makt INTO TABLE @DATA(lt_makt) WHERE spras = @sy-langu. " 第二步:按月汇总销售额,放入临时结果,再按物料号+月份建立索引 SELECT matnr, zmonth, SUM( zamount ) AS amount FROM ztp_fact INTO TABLE @DATA(lt_fact) GROUP BY matnr, zmonth. " 第三步:循环物料主数据,构造输出行 LOOP AT lt_makt INTO DATA(ls_makt). CLEAR: ls_key. ls_key-matnr = ls_makt-matnr. " 构建一行动态结构 ASSIGN <fs_table> TO <fs_line>. " 注意,这里要用APPEND后再ASSIGN,示例从略 ASSIGN COMPONENT 'MATNR' OF STRUCTURE <fs_line> TO <fs_field>. <fs_field> = ls_makt-matnr. ASSIGN COMPONENT 'MAKTX' OF STRUCTURE <fs_line> TO <fs_field>. <fs_field> = ls_makt-maktx. " 填充每个月份列 LOOP AT mt_dynamic_cols INTO DATA(ls_col). ASSIGN COMPONENT ls_col-fieldname OF STRUCTURE <fs_line> TO <fs_field>. IF sy-subrc = 0. READ TABLE lt_fact INTO DATA(ls_fact) WITH KEY matnr = ls_makt-matnr zmonth = ls_col-month. IF sy-subrc = 0. <fs_field> = ls_fact-amount. ENDIF. ENDIF. ENDLOOP. " 追加到内表 APPEND <fs_line> TO <fs_table>. ENDLOOP. ENDMETHOD.这个例子里有几处需要特别提醒的坑。
第一个是ASSIGN <fs_table> TO <fs_line>这种写法,实际上是用一个数据字段符号指向内表行。但内表是动态的,LOOP AT没法直接对动态内表做循环操作,所以这里用的是先ASSIGN行字段符号、填充、再APPEND的方式。
第二个是每轮循环都要重新ASSIGN行字段符号。因为APPEND之后工作区已经复制进内表,下一轮要OVERWRITE新工作区,否则会一直在同一块内存上改来改去,最终导致所有行内容一样。这个坑在动态内表场景下特别容易犯。
第三个是READ TABLE ... WITH KEY的用法。动态内表场景下,每次填充月份列都要查一次事实表,循环里这样做性能极差。优化办法是把事实表结果按物料号分组成索引,或者用LOOP AT lt_fact WHERE matnr = ...配合二分查找。实际项目里数量上去后,这个性能瓶颈会很明显。
5.4 完整展示调用
数据填充完成后,调用ALV展示前还要把字段目录设置完善。展示的核心代码:
METHOD display_alv. DATA: lo_grid TYPE REF TO cl_gui_alv_grid, lo_container TYPE REF TO cl_gui_custom_container. CREATE OBJECT lo_container EXPORTING container_name = 'CC_ALV'. CREATE OBJECT lo_grid EXPORTING i_parent = lo_container. " 字段目录必须在set_table前build完成 lo_grid->set_table_for_first_display( EXPORTING i_structure_name = '' is_layout = ls_layout CHANGING it_outtab = <fs_table> it_fieldcatalog = mt_fcat it_sort = lt_sort ). ENDMETHOD.i_structure_name传空是因为内表结构不是数据字典里定义的结构,而是动态生成的。it_outtab这里可以传字段符号指向的动态内表,调用接口接受泛型表参数。is_layout建议设置cwidth_opt = 'X',让列宽根据内容自动调整。
布局设置里还要注意no_authority_check等权限控制相关参数,动态列场景下如果列本身受权限管控,最好在字段目录或取数阶段就过滤,不要把权限逻辑放到前端展示层。
6. 从实战中踩出来的坑点清单
6.1 列顺序错乱与重复
动态列最容易出的问题是列顺序和预期不一致。原因大多是字段目录构建顺序与内表组件顺序不一致,或者内表组件在迭代时偶发顺序变化。解决方案很简单:组件和字段目录都用同一个mt_dynamic_cols驱动,保证顺序同源,不要在两端各自维护。
另一个完美重复列的问题,多发生在动态列名生成规则不严谨时。比如跨年月份M202401和M2401,格式不统一可能被当成两列。务必统一生成规则,并且生成后做一次去重和排序校验。
6.2 类型不匹配导致的DUMP
动态列类型如果和数据实际填充类型不一致,运行时大概率直接DUMP。尤其是金额字段,数据字典里定义成CURR类型后,ALV自动带上货币单位逻辑,如果填充时直接塞了个STRING进去,前端的数值格式化立刻出问题。
建议所有动态列的类型模板都以数据字典元素为准,不要自己硬编码inttype和intlen。如果确实需要手工指定,优先使用C、N、P等基础类型,并确保填充内容能安全转换。
6.3 大数据量场景的性能问题
动态内表在数据量较大时,有两个性能瓶颈。
第一个是动态组件ASSIGN COMPONENT的性能。每次从几万行动态表里取列值,都要做一次动态解析,比静态字段符号访问慢10倍以上。优化办法:能用静态结构时就别用动态,或者加一层“列名到组件索引”的映射缓存。
第二个是逐行APPEND的性能。动态内表无法直接INSERT整批数据,只能逐行追加。数据量大时建议先确认内表不需要排序,使用APPEND LINES OF批量写入会快很多。
6.4 字段目录与内表不同步
这是动态列开发里最隐蔽的坑。字段目录LVC_T_FCAT是按列名输出的,如果字段目录里有个列名在内表组件里不存在,ALV展示时该列会是空的,程序也不会报错。反过来内表里有列但字段目录里没有,这一列就直接消失。
排查方法:在SET_TABLE_FOR_FIRST_DISPLAY之前写一段检查逻辑,把字段目录的fieldname集合和内表组件名集合做差集,打印或断点提示。这个习惯能省下大量调显示问题的痛苦时间。
6.5 动态列的排序、过滤与合计
动态列的sort、filter、do_sum等属性,在ALV里都算功能增强。实际操作中要注意这些属性都得按动态列名逐一设置,不能有遗漏。比如用户要对“4月销售额”列排序,如果字段目录里没给这一列配sort_qty,点击列头排序时会触发运行时错误。
更高阶的做法是给ALV容器设置事件处理器,在ON_F1、ON_HOTSPOT_CLICK这些事件里通过sender->get_current_fieldname拿到列名,再根据列名做响应。动态列下,所有的判断逻辑都要围绕列名做分支,代码结构要清晰,别把所有分支堆进一个方法里。
7. 写在最后的几条实操建议
动态列显示这套技术,落地一年多来我最大的感触是:技术本身并不复杂,真正复杂的是把“动态”这个词变成可维护的抽象层。
如果你手头的报表还处于“需求只是隐藏几列”的阶段,别急着上动态内表。用字段目录控制显隐就行,代码简单、逻辑直观,后续接手的人不至于骂人。等真的出现“列数不固定”的需求,再引入动态内表。
工程实践上再加两条提醒。第一,动态列的元数据一定要集中管理,用内部表存列名、类型、标题、来源表达式,所有下游功能都从这个元数据出发,别散落在各种方法里。第二,动态ALV展示前加一道校验函数,检查字段目录和内表组件的一致性,这个函数我建议直接沉淀成通用工具,所有动态列报表都复用。
动态列显示本质上是把报表从“固定结构展示”升级为“结构化数据到展示列的动态映射”。想清楚这层映射关系,代码写起来就有章法了。