SAP ABAP 实战:SAP GUI上做 HTML 看板报表

本文讲一种在 SAP 里做"现代化网页报表"的架构:ABAP 只负责取数和校验,页面(HTML/CSS/JS)放在 SMW0 模板里,数据以 JSON 注入 。读完你能照着做出一张可筛选、可切换看板、小计总计自动重算的报表,并知道它怎么防止"模板和程序传输不同步"这类线上事故。

一、要解决什么问题

需求:做一张"按分组 × 类别 × 月份"的数量汇总报表,要求:

  1. 页面上能再筛选(分组、类别、月份),小计和总计随筛选实时重算;
  2. 有"经典表格"和"看板"(KPI、堆叠柱、排行、热力图)两种视图,可来回切换;
  3. 数据量不大(几千行),但改样式、改布局要快,不想每次都改 ABAP、走传输、等审批。

传统 ALV 做不了第 1、2 条;在 ABAP 里一行行拼 HTML 字符串,则第 3 条是噩梦------改一个颜色要动代码。

二、总体架构

复制代码
┌─────────────────────┐   每日背景作业写入    ┌────────────────┐
│ 源数据(任意数据表)│ ───────────────────▶ │ 快照表(按日期) │
└─────────────────────┘                       └───────┬────────┘
                                                      │ SELECT
┌─────────────────────────────────────────────────────▼────────┐
│ ABAP 报表                                                     │
│  1. get_data      读快照 + 主数据                            │
│  2. load_template 从 SMW0 读 HTML 模板(二进制,UTF-8)          │
│  3. check_template 检查版本/占位符/元素/标签/事件                │
│  4. build_json    内表 → JSON(并转义)→ 解析校验               │
│  5. replace       把 JSON 换进模板里的占位符                    │
│  6. display       docking 容器 + cl_gui_html_viewer 展示        │
└─────────────────────────────┬────────────────────────────────┘
                              │ SAPEVENT 回传(JS 错误 / 合计核对值)
                  ┌───────────▼───────────┐
                  │ 页面 JS:筛选、小计、看板 │
                  └───────────────────────┘

核心思想一句话:数据与展示分离,模板是"资源",不是"代码"。

层 内容 谁来改 怎么发布
数据层 快照表、取数 SELECT ABAP 开发 TR
注入层 内表→JSON、模板检查 ABAP 开发 TR
展示层 HTML/CSS/JS 模板 前端/任何人 SMW0 上传(也可带 TR)

三、为什么用"快照表"而不是实时查

报表的数据来自一个每天由背景作业写入的快照表,主键里带快照日期(快照每天由背景作业生成)。好处:

  • 查询只做简单 WHERE datum = ...,毫秒级,不会拖慢生产;
  • 同一天的数据口径固定,不同日期的数字可以对比;
  • 页面上显示"数据写入时间",用户一看就知道是哪个时点的数。

选择画面里"快照日期"默认取 MAX( datum ),并提供 F4,只列表里已有的日期:

abap 复制代码
SELECT MAX( datum ) FROM zsnap_demo INTO @p_datum.
IF p_datum IS INITIAL.
  p_datum = sy-datum.
ENDIF.

F4 用 F4IF_INT_TABLE_VALUE_REQUEST,把 SELECT DISTINCT datum ... ORDER BY datum DESCENDING 的结果丢给它即可。

四、关键技术点一:用选择屏幕承载 HTML

不想另建 Dynpro?可以用代码定义的选择屏幕 9100,上面放一个 docking 容器:

abap 复制代码
SELECTION-SCREEN BEGIN OF SCREEN 9100 TITLE tit2.
SELECTION-SCREEN END OF SCREEN 9100.

" AT SELECTION-SCREEN OUTPUT 里,仅对 9100 建控件
CHECK sy-dynnr = '9100'.
CHECK go_dock IS INITIAL.
CREATE OBJECT go_dock
  EXPORTING repid = sy-repid dynnr = '9100'
            side  = cl_gui_docking_container=>dock_at_top
            ratio = 95
  EXCEPTIONS OTHERS = 1.
CREATE OBJECT go_html EXPORTING parent = go_dock EXCEPTIONS OTHERS = 1.

展示时 CALL SELECTION-SCREEN 9100.,返回后别忘了 free( ) 控件,否则再次进入会残留。

结果屏幕上不需要"执行/后台/变式"这些按钮,用 RS_SET_SELSCREEN_STATUS 的 p_exclude 排除掉(ONLI、CRET、PRIN、SJOB、SPOS、GET、VDEL、VSHO)。

abap 复制代码
  " (9100 用 CALL SELECTION-SCREEN 叫出,狀態是 %_CS,執行鍵的功能碼是 CRET)
  lt_excl = VALUE #( ( 'ONLI' ) ( 'CRET' ) ( 'PRIN' ) ( 'SJOB' ) ( 'SPOS' ) ( 'GET' ) ( 'VDEL' ) ( 'VSHO' ) ).
  CALL FUNCTION 'RS_SET_SELSCREEN_STATUS'
    EXPORTING
      p_status  = sy-pfkey
    TABLES
      p_exclude = lt_excl.

五、关键技术点二:模板放进 SMW0

5.1 读取

SMW0 的"二进制数据"类型对象,用 WWWDATA_IMPORT 读出来是 W3MIME 行表(每行 1022 字节左右),要自己拼回 xstring:

abap 复制代码
  DATA: ls_key  TYPE wwwdatatab,
        lt_mime TYPE STANDARD TABLE OF w3mime WITH DEFAULT KEY,
        lv_xtpl TYPE xstring,
        lv_size TYPE c LENGTH 255,
        lv_len  TYPE i.

  FIELD-SYMBOLS <ls_mime> TYPE w3mime.

  CLEAR cv_tpl.
  ls_key-relid = 'MI'.
  ls_key-objid = gc_tpl_objid.

  CALL FUNCTION 'WWWDATA_IMPORT'
    EXPORTING
      key               = ls_key
    TABLES
      mime              = lt_mime
    EXCEPTIONS
      wrong_object_type = 1
      import_error      = 2
      OTHERS            = 3.
  IF sy-subrc <> 0 OR lt_mime IS INITIAL.
    APPEND |SMW0 找不到二進位物件 { gc_tpl_objid }| TO ct_msg.
    RETURN.
  ENDIF.

  LOOP AT lt_mime ASSIGNING <ls_mime>.
    CONCATENATE lv_xtpl <ls_mime>-line INTO lv_xtpl IN BYTE MODE.
  ENDLOOP.

  CALL FUNCTION 'WWWPARAMS_READ'
    EXPORTING
      relid            = 'MI'
      objid            = gc_tpl_objid
      name             = 'filesize'
    IMPORTING
      value            = lv_size
    EXCEPTIONS
      entry_not_exists = 1
      OTHERS           = 2.
  IF sy-subrc = 0 AND lv_size IS NOT INITIAL AND lv_size CO ' 0123456789'.
    lv_len = lv_size.
    IF lv_len > 0 AND lv_len < xstrlen( lv_xtpl ).
      lv_xtpl = lv_xtpl(lv_len).
    ENDIF.
  ENDIF.

坑 1:尾部补字节。 最后一行不足会补零,要用 WWWPARAMS_READ 读参数 filesize,把 xstring 截到真实长度,否则末尾出现乱码。

坑 2:BOM。 UTF-8 文件开头可能有 EF BB BF,要判断并去掉:

abap 复制代码
IF xstrlen( lv_xtpl ) >= 3
   AND lv_xtpl(3) = cl_abap_char_utilities=>byte_order_mark_utf8.
  lv_xtpl = lv_xtpl+3.
ENDIF.
cv_tpl = cl_abap_conv_codepage=>create_in( )->convert( lv_xtpl ).

5.2 注入数据

模板里留一个唯一的占位符,平时是合法的 JS(本地用浏览器直接打开模板也能跑):

html 复制代码
<script>
var PAYLOAD = /*@@DATA@@*/null;
</script>

ABAP 侧一句 replace 即可:

abap 复制代码
gv_html = replace( val = lv_tpl sub = `/*@@DATA@@*/null` with = lv_json ).

好处:前端同学拿到模板,本地放一份 mock JSON 就能调样式,完全不需要 SAP。

六、关键技术点三:内表转 JSON

用 /ui2/cl_json=>serialize,先定义好"页面用的结构",字段名就是 JS 里用的名字(越短越省流量):

abap 复制代码
TYPES: BEGIN OF ty_row,
         w TYPE string,   " 分组
         b TYPE string,   " 类别
         m TYPE string,   " 月份
         q TYPE p LENGTH 13 DECIMALS 3,   " 数量
       END OF ty_row,
       BEGIN OF ty_payload,
         meta TYPE ty_meta,
         data TYPE STANDARD TABLE OF ty_row WITH EMPTY KEY,
       END OF ty_payload.

cv_json = /ui2/cl_json=>serialize(
            data        = ls_payload
            pretty_name = /ui2/cl_json=>pretty_mode-low_case ).

注入 <script> 前必须做三件事,否则偶发"页面空白":

字符 问题 处理
< 数据里出现 </script> 会提前结束脚本块 换成 <
U+2028 / U+2029 ES5 里算换行,字符串直接断掉 换成 `
/
`
非法 JSON 前面都对但内容有误 用 cl_sxml_string_reader 完整读一遍,抛异常就拦下

注意:转义用的反斜线和 u 在 ABAP 里要分开拼(\` && `u003c),否则某些编辑器会把它当转义吃掉。

七、关键技术点四:防呆------不让"半套"上线

这套架构的最大风险是:程序和模板分两个传输,只传了一边。于是做了三道检查。

1)版本号对账。 模板里放 <meta name="ztpl-version" content="1">,程序里常量也写 1。不一致就拒绝显示。注意 ABAP 的 PCRE 默认是 extended 模式,会忽略空格,要加 (?-x):

abap 复制代码
FIND PCRE `(?-x)<meta name="ztpl-version" content="(\d+)">`
     IN iv_tpl SUBMATCHES lv_ver.

2)结构检查。 占位符恰好 1 个;页面程序要用的元素 id 各 1 个;html/head/body/script/style 标签开闭成对;必须包含 SAPEVENT:(回传通道)。

3)运行期回传。 页面里:

javascript 复制代码
window.onerror = function (msg, src, line, col) {
  location.href = 'SAPEVENT:jserr?msg=' + encodeURIComponent(msg)
                + '&line=' + line + '&col=' + col;
};

加载完成后,页面在不筛选的状态下,把"总行数、各类合计、表格总计、看板总计"一并回传:

javascript 复制代码
location.href = 'SAPEVENT:ready?rows=' + rows + '&q01=' + q01 + '&tbl=' + tbl;

ABAP 侧用 cl_gui_html_viewer=>m_id_sapevent 注册事件(appl_event = abap_false,系统事件,不触发 PAI),把回传值和内表重新汇总的数字逐个比对,容差 0.001,不一致就弹窗:

abap 复制代码
lt_evt = VALUE #( ( eventid    = cl_gui_html_viewer=>m_id_sapevent
                    appl_event = abap_false ) ).
go_html->set_registered_events( EXPORTING events = lt_evt EXCEPTIONS OTHERS = 1 ).
SET HANDLER lcl_html_event=>on_sapevent FOR go_html.

这样 JS 报错、前端算错,用户第一时间看到提示,而不是拿着错数据去开会。

另外加了一个"只检查模板"的复选框,结果用 WRITE 输出,不依赖 GUI 控件,背景作业也能跑------传输后挂个作业就能确认"程序和模板配套"。

八、关键技术点五:HTML 怎么送到控件

不要用字符行表 load_data(要处理 255 宽度切行,容易切断标签)。改成UTF-8 二进制直传:

abap 复制代码
lv_xhtml = cl_abap_conv_codepage=>create_out( )->convert( lv_html ).
CONCATENATE cl_abap_char_utilities=>byte_order_mark_utf8 lv_xhtml
       INTO lv_xhtml IN BYTE MODE.
lv_size = xstrlen( lv_xhtml ).
lt_bin  = cl_bcs_convert=>xstring_to_solix( lv_xhtml ).

go_html->load_data( EXPORTING size = lv_size
                    IMPORTING assigned_url = lv_url
                    CHANGING  data_table   = lt_bin
                    EXCEPTIONS OTHERS = 1 ).
go_html->show_url( url = lv_url ).

字节原样写到前端,不经 SAP GUI 转码,中文不会乱。

九、使用示例

9.1 用到的另外的小知识点

免选择屏幕的实现要点(sy-calld 防止死循环):

abap 复制代码
INITIALIZATION.
  IF sy-tcode = 'ZDEMO_R' AND sy-calld IS INITIAL.
    SUBMIT zdemo_report WITH p_datum = p_datum AND RETURN.
    LEAVE PROGRAM.
  ENDIF.

9.2 改页面怎么做

  1. 本地改模板(浏览器直接打开,用 mock JSON 调样式);
  2. SMW0 → 二进制数据 → 更新上传;
  3. 如果只改样式、没动结构,不需要改 ABAP,也不用发 TR;
  4. 如果动了元素 id 或占位符,同时抬高两边的版本号,一起传输。

9.3 排错速查

现象 原因 处理
提示"SMW0 找不到二进制对象" 没传模板或名字不对 SMW0 检查对象名
提示模板版本不一致 只传了程序或只传了模板 补传另一边
页面空白,无提示 数据里有 </script> 或 U+2028 确认转义逻辑未被删
中文乱码 字符行表 load_data 或缺 BOM 改 UTF-8 二进制直传
模板末尾多出乱码 没按 filesize 截断 读 filesize 参数截断
弹窗"页面程序错误 第 N 行" JS 语法或运行错误 N 即模板行号,直接定位
弹窗"合计不一致" 前端汇总逻辑与 ABAP 不同 对照提示逐项查
背景执行无画面 HTML 控件需要前台 GUI 背景只用"检查模板"模式

十、几点经验

  1. 模板视为资源,检查要自动化。 凡是"代码之外的东西"(模板、配置),都应该有"和代码配套"的校验。
  2. 前后端各算一遍,互相核对。 页面算的数字用 SAPEVENT 回传,由 ABAP 以原始数据复核,比人工抽查靠谱。
  3. 数据量别过大。 整个数据集一次性塞进页面,适合几千到几万行;再大应考虑服务端分页或预聚合。
  4. 控件释放不能省。 free( ) 掉 html viewer 和 docking container,避免残留。
  5. 选择文本动态设置。 如果程序文本池只有一种语言,用 SELECTION_TEXTS_MODIFY 在运行时按需补,免得其他语言登录时只显示变量名。

十一、小结

要点 做法
取数 每日快照 + 简单 WHERE
页面 SMW0 二进制模板,JS 自处理筛选/小计
注入 /ui2/cl_json 序列化 + <、U+2028/2029 转义 + sXML 校验
防呆 版本号、结构检查、JS 错误回传、合计核对
发布 改样式只传模板,改结构程序与模板同传

这套"ABAP 取数校验 + SMW0 模板展示"的组合,既保留了 SAP 内的权限和数据安全,又拿到了前端的灵活度,非常适合"报表型小工具"。

如果觉得有用,欢迎点赞收藏;有问题评论区交流。

相关推荐
时间的情敌30 分钟前
Cesium 建筑单体化、3DTiles 分类、楼层显隐、分户高亮(BIM + 倾斜孪生高频)
前端·3d·cesium
IT_陈寒40 分钟前
JavaScript项目实战经验分享:从内存泄漏到性能优化的血泪史
前端·人工智能·后端
恋猫de小郭2 小时前
Flutter 3.50 & Dart 3.14 发布,快来看看有什么更新吧
android·前端·flutter
非优秀程序员10 小时前
第10章 千团出世|《互联网产品恩仇录》
前端
非优秀程序员10 小时前
第11章 小高在对面|《互联网产品恩仇录》
前端
非优秀程序员10 小时前
第14章 只剩美团|《互联网产品恩仇录》
前端
非优秀程序员10 小时前
第09章 老乡何必为难老乡|《互联网产品恩仇录》
前端
非优秀程序员10 小时前
第03章 三大门户,烽烟起于瀛海威|《互联网产品恩仇录》
前端
程序员大阳14 小时前
HTML+CSS+JS对话式实战(5)-活动管理页之查看报名名单的实现
javascript·css·html·查看报名名单