实现效果:

源码实现:
CLASS zcl_ztest01_web_maint DEFINITION
PUBLIC
FINAL
CREATE PUBLIC .
PUBLIC SECTION.
INTERFACES if_http_extension.
PRIVATE SECTION.
TYPES:
BEGIN OF ty_field,
name TYPE fieldname,
json_name TYPE string,
label TYPE string,
is_key TYPE abap_bool,
readonly TYPE abap_bool,
END OF ty_field.
TYPES ty_field_list TYPE STANDARD TABLE OF ty_field WITH EMPTY KEY.
* CONSTANTS maintained_table TYPE tabname VALUE 'ZTEST01'.
CONSTANTS maintained_table TYPE tabname VALUE 'ZTEST01'.
CONSTANTS content_type_html TYPE string VALUE 'text/html; charset=utf-8'.
CONSTANTS content_type_json TYPE string VALUE 'application/json; charset=utf-8'.
CONSTANTS max_rows TYPE i VALUE 1000.
CONSTANTS max_response_rows TYPE i VALUE 200.
METHODS handle_page
IMPORTING
server TYPE REF TO if_http_server.
METHODS handle_list
IMPORTING
server TYPE REF TO if_http_server.
METHODS handle_save
IMPORTING
server TYPE REF TO if_http_server.
METHODS handle_delete
IMPORTING
server TYPE REF TO if_http_server.
METHODS get_fields
RETURNING
VALUE(result) TYPE ty_field_list.
METHODS get_request_values
IMPORTING
server TYPE REF TO if_http_server
fields TYPE ty_field_list
RETURNING
VALUE(result) TYPE string_table.
METHODS append_json_fields
IMPORTING
fields TYPE ty_field_list
CHANGING
json TYPE string.
METHODS append_json_rows
IMPORTING
fields TYPE ty_field_list
keyword TYPE string
CHANGING
json TYPE string.
METHODS append_json_row
IMPORTING
fields TYPE ty_field_list
row TYPE any
CHANGING
json TYPE string.
METHODS row_matches_keyword
IMPORTING
fields TYPE ty_field_list
row TYPE any
keyword TYPE string
RETURNING
VALUE(result) TYPE abap_bool.
METHODS write_json
IMPORTING
server TYPE REF TO if_http_server
json TYPE string.
METHODS write_error
IMPORTING
server TYPE REF TO if_http_server
status_code TYPE i
reason TYPE string
message TYPE string.
METHODS escape_json_value
IMPORTING
value TYPE string
RETURNING
VALUE(result) TYPE string.
ENDCLASS.
CLASS ZCL_ZTEST01_WEB_MAINT IMPLEMENTATION.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_ZTEST01_WEB_MAINT->IF_HTTP_EXTENSION~HANDLE_REQUEST
* +-------------------------------------------------------------------------------------------------+
* | [--->] SERVER TYPE REF TO IF_HTTP_SERVER
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD if_http_extension~handle_request.
DATA(action) = server->request->get_form_field( `action` ).
CASE action.
WHEN ``.
handle_page( server ).
WHEN `list`.
handle_list( server ).
WHEN `save`.
handle_save( server ).
WHEN `delete`.
handle_delete( server ).
WHEN OTHERS.
write_error(
server = server
status_code = 404
reason = `Not Found`
message = `Unsupported action` ).
ENDCASE.
ENDMETHOD.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Private Method ZCL_ZTEST01_WEB_MAINT->HANDLE_PAGE
* +-------------------------------------------------------------------------------------------------+
* | [--->] SERVER TYPE REF TO IF_HTTP_SERVER
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD handle_page.
DATA html_lines TYPE STANDARD TABLE OF string WITH EMPTY KEY.
INSERT `<!doctype html>` INTO TABLE html_lines.
INSERT `<html lang="zh-CN">` INTO TABLE html_lines.
INSERT `<head>` INTO TABLE html_lines.
INSERT ` <meta charset="utf-8">` INTO TABLE html_lines.
INSERT ` <meta name="viewport" content="width=device-width, initial-scale=1">` INTO TABLE html_lines.
INSERT ` <title>通用表维护</title>` INTO TABLE html_lines.
INSERT ` <style>` INTO TABLE html_lines.
INSERT ` :root { --color_primary: #0066b3; --color_text: #1f2937; --color_muted: #667085; --color_border: #d7deea; --color_bg: #f4f7fb; --color_panel: #ffffff; --color_danger: #c9352b; --color_success: #18794e; }` INTO TABLE html_lines.
INSERT ` * { box-sizing: border-box; }` INTO TABLE html_lines.
INSERT ` body { margin: 0; font-family: "Segoe UI", "Microsoft YaHei", sans-serif; color: var(--color_text); background: linear-gradient(180deg, #eef4fb 0, var(--color_bg) 220px); }` INTO TABLE html_lines.
INSERT ` .app { min-height: 100vh; padding: 24px; }` INTO TABLE html_lines.
INSERT ` .shell { max-width: 1180px; margin: 0 auto; }` INTO TABLE html_lines.
INSERT ` .header { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; margin-bottom: 18px; }` INTO TABLE html_lines.
INSERT ` .title { margin: 0; font-size: 24px; font-weight: 650; letter-spacing: 0; }` INTO TABLE html_lines.
INSERT ` .subtitle { margin: 6px 0 0; color: var(--color_muted); font-size: 13px; }` INTO TABLE html_lines.
INSERT ` .panel { background: var(--color_panel); border: 1px solid var(--color_border); border-radius: 6px; box-shadow: 0 8px 24px rgba(16, 24, 40, .08); }` INTO TABLE html_lines.
INSERT ` .toolbar { display: flex; flex-wrap: wrap; gap: 8px; padding: 14px; border-bottom: 1px solid var(--color_border); }` INTO TABLE html_lines.
INSERT ` .input { height: 34px; min-width: 220px; padding: 0 10px; border: 1px solid var(--color_border); border-radius: 4px; background: #fff; color: var(--color_text); font-size: 14px; outline: none; }` INTO TABLE html_lines.
INSERT ` .input:focus { border-color: var(--color_primary); box-shadow: 0 0 0 3px rgba(0, 102, 179, .12); }` INTO TABLE html_lines.
INSERT ` .button { height: 34px; padding: 0 14px; border: 1px solid var(--color_primary); border-radius: 4px; color: #fff; background: var(--color_primary); font-size: 14px; cursor: pointer; }` INTO TABLE html_lines.
INSERT ` .button:disabled { cursor: not-allowed; opacity: .55; }` INTO TABLE html_lines.
INSERT ` .button_secondary { color: var(--color_primary); background: #fff; }` INTO TABLE html_lines.
INSERT ` .button_danger { border-color: var(--color_danger); color: var(--color_danger); background: #fff; }` INTO TABLE html_lines.
INSERT ` .message { min-height: 32px; padding: 8px 14px; color: var(--color_muted); font-size: 13px; }` INTO TABLE html_lines.
INSERT ` .message_error { color: var(--color_danger); }` INTO TABLE html_lines.
INSERT ` .message_success { color: var(--color_success); }` INTO TABLE html_lines.
INSERT ` .table_wrap { overflow-x: auto; }` INTO TABLE html_lines.
INSERT ` table { width: 100%; border-collapse: collapse; }` INTO TABLE html_lines.
INSERT ` th, td { padding: 11px 12px; border-top: 1px solid var(--color_border); text-align: left; font-size: 14px; white-space: nowrap; }` INTO TABLE html_lines.
INSERT ` th { background: #f0f4f9; font-weight: 600; color: #344054; }` INTO TABLE html_lines.
INSERT ` td.empty { padding: 32px 12px; text-align: center; color: var(--color_muted); }` INTO TABLE html_lines.
INSERT ` .actions { display: flex; gap: 6px; }` INTO TABLE html_lines.
INSERT ` .dialog_mask { position: fixed; inset: 0; display: none; align-items: center; justify-content: center; padding: 16px; background: rgba(15, 23, 42, .38); }` INTO TABLE html_lines.
INSERT ` .dialog { width: min(560px, 100%); max-height: min(720px, 92vh); overflow: auto; padding: 18px; background: #fff; border-radius: 6px; box-shadow: 0 18px 45px rgba(15, 23, 42, .22); }` INTO TABLE html_lines.
INSERT ` .dialog h2 { margin: 0 0 14px; font-size: 18px; }` INTO TABLE html_lines.
INSERT ` .form_item { margin-bottom: 12px; }` INTO TABLE html_lines.
INSERT ` .form_item label { display: block; margin-bottom: 5px; color: #344054; font-size: 13px; }` INTO TABLE html_lines.
INSERT ` .form_item .input { width: 100%; }` INTO TABLE html_lines.
INSERT ` .dialog_footer { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }` INTO TABLE html_lines.
INSERT ` @media (max-width: 720px) { .app { padding: 14px; } .header { align-items: flex-start; flex-direction: column; } .input { width: 100%; } .button { flex: 1; } }` INTO TABLE html_lines.
INSERT ` </style>` INTO TABLE html_lines.
INSERT `</head>` INTO TABLE html_lines.
INSERT `<body>` INTO TABLE html_lines.
INSERT ` <main class="app">` INTO TABLE html_lines.
INSERT ` <section class="shell">` INTO TABLE html_lines.
INSERT ` <div class="header"><div><h1 id="pageTitle" class="title">通用表维护</h1><p id="pageSubtitle" class="subtitle"></p></div></div>` INTO TABLE html_lines.
INSERT ` <section class="panel">` INTO TABLE html_lines.
INSERT ` <div class="toolbar">` INTO TABLE html_lines.
INSERT ` <input id="keyword" class="input" placeholder="输入关键字查询">` INTO TABLE html_lines.
INSERT ` <button id="queryButton" class="button button_secondary" type="button">查询</button>` INTO TABLE html_lines.
INSERT ` <button id="newButton" class="button" type="button">新增</button>` INTO TABLE html_lines.
INSERT ` </div>` INTO TABLE html_lines.
INSERT ` <div id="message" class="message"></div>` INTO TABLE html_lines.
INSERT ` <div class="table_wrap"><table><thead><tr id="tableHead"></tr></thead><tbody id="tableBody"></tbody></table></div>` INTO TABLE html_lines.
INSERT ` </section>` INTO TABLE html_lines.
INSERT ` </section>` INTO TABLE html_lines.
INSERT ` </main>` INTO TABLE html_lines.
INSERT ` <div id="dialogMask" class="dialog_mask" role="dialog" aria-modal="true">` INTO TABLE html_lines.
INSERT ` <div class="dialog">` INTO TABLE html_lines.
INSERT ` <h2>维护记录</h2>` INTO TABLE html_lines.
INSERT ` <div id="formBody"></div>` INTO TABLE html_lines.
INSERT ` <div class="dialog_footer">` INTO TABLE html_lines.
INSERT ` <button id="cancelButton" class="button button_secondary" type="button">取消</button>` INTO TABLE html_lines.
INSERT ` <button id="saveButton" class="button" type="button">保存</button>` INTO TABLE html_lines.
INSERT ` </div>` INTO TABLE html_lines.
INSERT ` </div>` INTO TABLE html_lines.
INSERT ` </div>` INTO TABLE html_lines.
INSERT ` <script>` INTO TABLE html_lines.
INSERT ` const buildApiUrl = (action, params) => {` INTO TABLE html_lines.
INSERT ` const url = new URL(window.location.href);` INTO TABLE html_lines.
INSERT ` url.searchParams.set("action", action);` INTO TABLE html_lines.
INSERT ` Object.entries(params || {}).forEach(([key, value]) => url.searchParams.set(key, value));` INTO TABLE html_lines.
INSERT ` return url.toString();` INTO TABLE html_lines.
INSERT ` };` INTO TABLE html_lines.
INSERT ` const state = { fields: [], rows: [], current: null };` INTO TABLE html_lines.
INSERT ` const getElement = (id) => document.getElementById(id);` INTO TABLE html_lines.
INSERT ` const setMessage = (text, type) => {` INTO TABLE html_lines.
INSERT ` const element = getElement("message");` INTO TABLE html_lines.
INSERT ` element.textContent = text || "";` INTO TABLE html_lines.
INSERT ` element.className = "message" + (type ? " message_" + type : "");` INTO TABLE html_lines.
INSERT ` };` INTO TABLE html_lines.
INSERT ` const setBusy = (busy) => {` INTO TABLE html_lines.
INSERT ` getElement("queryButton").disabled = busy;` INTO TABLE html_lines.
INSERT ` getElement("newButton").disabled = busy;` INTO TABLE html_lines.
INSERT ` getElement("saveButton").disabled = busy;` INTO TABLE html_lines.
INSERT ` };` INTO TABLE html_lines.
INSERT ` const createCell = (text) => {` INTO TABLE html_lines.
INSERT ` const cell = document.createElement("td");` INTO TABLE html_lines.
INSERT ` cell.textContent = text || "";` INTO TABLE html_lines.
INSERT ` return cell;` INTO TABLE html_lines.
INSERT ` };` INTO TABLE html_lines.
INSERT ` const requestJson = async (url, options) => {` INTO TABLE html_lines.
INSERT ` const response = await fetch(url, options || {});` INTO TABLE html_lines.
INSERT ` const text = await response.text();` INTO TABLE html_lines.
INSERT ` let data = {};` INTO TABLE html_lines.
INSERT ` if (text) { try { data = JSON.parse(text); } catch (error) { data = { message: text }; } }` INTO TABLE html_lines.
INSERT ` if (!response.ok) { throw new Error(data.message || "请求失败"); }` INTO TABLE html_lines.
INSERT ` return data;` INTO TABLE html_lines.
INSERT ` };` INTO TABLE html_lines.
INSERT ` const renderHeader = () => {` INTO TABLE html_lines.
INSERT ` const head = getElement("tableHead");` INTO TABLE html_lines.
INSERT ` head.innerHTML = "";` INTO TABLE html_lines.
INSERT ` state.fields.forEach((field) => {` INTO TABLE html_lines.
INSERT ` const cell = document.createElement("th");` INTO TABLE html_lines.
INSERT ` cell.textContent = field.label || field.name;` INTO TABLE html_lines.
INSERT ` head.appendChild(cell);` INTO TABLE html_lines.
INSERT ` });` INTO TABLE html_lines.
INSERT ` const actionCell = document.createElement("th");` INTO TABLE html_lines.
INSERT ` actionCell.textContent = "操作";` INTO TABLE html_lines.
INSERT ` head.appendChild(actionCell);` INTO TABLE html_lines.
INSERT ` };` INTO TABLE html_lines.
INSERT ` const renderRows = () => {` INTO TABLE html_lines.
INSERT ` const body = getElement("tableBody");` INTO TABLE html_lines.
INSERT ` body.innerHTML = "";` INTO TABLE html_lines.
INSERT ` if (!state.rows.length) {` INTO TABLE html_lines.
INSERT ` const row = document.createElement("tr");` INTO TABLE html_lines.
INSERT ` const cell = document.createElement("td");` INTO TABLE html_lines.
INSERT ` cell.className = "empty";` INTO TABLE html_lines.
INSERT ` cell.colSpan = state.fields.length + 1;` INTO TABLE html_lines.
INSERT ` cell.textContent = "暂无数据";` INTO TABLE html_lines.
INSERT ` row.appendChild(cell);` INTO TABLE html_lines.
INSERT ` body.appendChild(row);` INTO TABLE html_lines.
INSERT ` return;` INTO TABLE html_lines.
INSERT ` }` INTO TABLE html_lines.
INSERT ` state.rows.forEach((item) => {` INTO TABLE html_lines.
INSERT ` const row = document.createElement("tr");` INTO TABLE html_lines.
INSERT ` state.fields.forEach((field) => row.appendChild(createCell(item[field.json_name])));` INTO TABLE html_lines.
INSERT ` const actionCell = document.createElement("td");` INTO TABLE html_lines.
INSERT ` const actions = document.createElement("div");` INTO TABLE html_lines.
INSERT ` actions.className = "actions";` INTO TABLE html_lines.
INSERT ` const editButton = document.createElement("button");` INTO TABLE html_lines.
INSERT ` editButton.className = "button button_secondary";` INTO TABLE html_lines.
INSERT ` editButton.type = "button";` INTO TABLE html_lines.
INSERT ` editButton.textContent = "编辑";` INTO TABLE html_lines.
INSERT ` editButton.addEventListener("click", () => openDialog(item));` INTO TABLE html_lines.
INSERT ` const deleteButton = document.createElement("button");` INTO TABLE html_lines.
INSERT ` deleteButton.className = "button button_danger";` INTO TABLE html_lines.
INSERT ` deleteButton.type = "button";` INTO TABLE html_lines.
INSERT ` deleteButton.textContent = "删除";` INTO TABLE html_lines.
INSERT ` deleteButton.addEventListener("click", () => deleteRow(item));` INTO TABLE html_lines.
INSERT ` actions.appendChild(editButton);` INTO TABLE html_lines.
INSERT ` actions.appendChild(deleteButton);` INTO TABLE html_lines.
INSERT ` actionCell.appendChild(actions);` INTO TABLE html_lines.
INSERT ` row.appendChild(actionCell);` INTO TABLE html_lines.
INSERT ` body.appendChild(row);` INTO TABLE html_lines.
INSERT ` });` INTO TABLE html_lines.
INSERT ` };` INTO TABLE html_lines.
INSERT ` const renderForm = (row) => {` INTO TABLE html_lines.
INSERT ` const formBody = getElement("formBody");` INTO TABLE html_lines.
INSERT ` formBody.innerHTML = "";` INTO TABLE html_lines.
INSERT ` state.fields.forEach((field) => {` INTO TABLE html_lines.
INSERT ` const wrapper = document.createElement("div");` INTO TABLE html_lines.
INSERT ` wrapper.className = "form_item";` INTO TABLE html_lines.
INSERT ` const label = document.createElement("label");` INTO TABLE html_lines.
INSERT ` label.textContent = field.label || field.name;` INTO TABLE html_lines.
INSERT ` label.setAttribute("for", "field_" + field.name);` INTO TABLE html_lines.
INSERT ` const input = document.createElement("input");` INTO TABLE html_lines.
INSERT ` input.id = "field_" + field.name;` INTO TABLE html_lines.
INSERT ` input.className = "input";` INTO TABLE html_lines.
INSERT ` input.dataset.field = field.name;` INTO TABLE html_lines.
INSERT ` input.value = row && row[field.json_name] ? row[field.json_name] : "";` INTO TABLE html_lines.
INSERT ` if (field.readonly && row) { input.readOnly = true; }` INTO TABLE html_lines.
INSERT ` wrapper.appendChild(label);` INTO TABLE html_lines.
INSERT ` wrapper.appendChild(input);` INTO TABLE html_lines.
INSERT ` formBody.appendChild(wrapper);` INTO TABLE html_lines.
INSERT ` });` INTO TABLE html_lines.
INSERT ` };` INTO TABLE html_lines.
INSERT ` const openDialog = (row) => {` INTO TABLE html_lines.
INSERT ` state.current = row || null;` INTO TABLE html_lines.
INSERT ` renderForm(row);` INTO TABLE html_lines.
INSERT ` getElement("dialogMask").style.display = "flex";` INTO TABLE html_lines.
INSERT ` };` INTO TABLE html_lines.
INSERT ` const closeDialog = () => { getElement("dialogMask").style.display = "none"; };` INTO TABLE html_lines.
INSERT ` const collectPayload = () => {` INTO TABLE html_lines.
INSERT ` const payload = {};` INTO TABLE html_lines.
INSERT ` document.querySelectorAll("[data-field]").forEach((input) => { payload[input.dataset.field] = input.value.trim(); });` INTO TABLE html_lines.
INSERT ` return payload;` INTO TABLE html_lines.
INSERT ` };` INTO TABLE html_lines.
INSERT ` const loadRows = async () => {` INTO TABLE html_lines.
INSERT ` setBusy(true);` INTO TABLE html_lines.
INSERT ` setMessage("正在查询...");` INTO TABLE html_lines.
INSERT ` try {` INTO TABLE html_lines.
INSERT ` const keyword = getElement("keyword").value.trim();` INTO TABLE html_lines.
INSERT ` const data = await requestJson(buildApiUrl("list", { keyword }));` INTO TABLE html_lines.
INSERT ` state.fields = Array.isArray(data.fields) ? data.fields : [];` INTO TABLE html_lines.
INSERT ` state.rows = Array.isArray(data.rows) ? data.rows : [];` INTO TABLE html_lines.
INSERT ` getElement("pageTitle").textContent = data.table + " 表维护";` INTO TABLE html_lines.
INSERT ` getElement("pageSubtitle").textContent = "表名由 ABAP 常量 maintained_table 控制";` INTO TABLE html_lines.
INSERT ` renderHeader();` INTO TABLE html_lines.
INSERT ` renderRows();` INTO TABLE html_lines.
INSERT ` setMessage("查询完成,共 " + state.rows.length + " 条", "success");` INTO TABLE html_lines.
INSERT ` } catch (error) { setMessage(error.message, "error"); } finally { setBusy(false); }` INTO TABLE html_lines.
INSERT ` };` INTO TABLE html_lines.
INSERT ` const saveRow = async () => {` INTO TABLE html_lines.
INSERT ` const payload = collectPayload();` INTO TABLE html_lines.
INSERT ` setBusy(true);` INTO TABLE html_lines.
INSERT ` try {` INTO TABLE html_lines.
INSERT ` await requestJson(buildApiUrl("save"), {` INTO TABLE html_lines.
INSERT ` method: "POST",` INTO TABLE html_lines.
INSERT ` headers: { "Content-Type": "application/json" },` INTO TABLE html_lines.
INSERT ` body: JSON.stringify(payload)` INTO TABLE html_lines.
INSERT ` });` INTO TABLE html_lines.
INSERT ` closeDialog();` INTO TABLE html_lines.
INSERT ` await loadRows();` INTO TABLE html_lines.
INSERT ` } catch (error) { setMessage(error.message, "error"); } finally { setBusy(false); }` INTO TABLE html_lines.
INSERT ` };` INTO TABLE html_lines.
INSERT ` const deleteRow = async (row) => {` INTO TABLE html_lines.
INSERT ` const payload = {};` INTO TABLE html_lines.
INSERT ` state.fields.filter((field) => field.is_key).forEach((field) => { payload[field.name] = row[field.json_name]; });` INTO TABLE html_lines.
INSERT ` if (!confirm("确认删除当前记录?")) { return; }` INTO TABLE html_lines.
INSERT ` setBusy(true);` INTO TABLE html_lines.
INSERT ` try {` INTO TABLE html_lines.
INSERT ` await requestJson(buildApiUrl("delete"), {` INTO TABLE html_lines.
INSERT ` method: "POST",` INTO TABLE html_lines.
INSERT ` headers: { "Content-Type": "application/json" },` INTO TABLE html_lines.
INSERT ` body: JSON.stringify(payload)` INTO TABLE html_lines.
INSERT ` });` INTO TABLE html_lines.
INSERT ` await loadRows();` INTO TABLE html_lines.
INSERT ` } catch (error) { setMessage(error.message, "error"); } finally { setBusy(false); }` INTO TABLE html_lines.
INSERT ` };` INTO TABLE html_lines.
INSERT ` getElement("queryButton").addEventListener("click", loadRows);` INTO TABLE html_lines.
INSERT ` getElement("newButton").addEventListener("click", () => openDialog(null));` INTO TABLE html_lines.
INSERT ` getElement("cancelButton").addEventListener("click", closeDialog);` INTO TABLE html_lines.
INSERT ` getElement("saveButton").addEventListener("click", saveRow);` INTO TABLE html_lines.
INSERT ` getElement("keyword").addEventListener("keydown", (event) => { if (event.key === "Enter") { loadRows(); } });` INTO TABLE html_lines.
INSERT ` loadRows();` INTO TABLE html_lines.
INSERT ` </script>` INTO TABLE html_lines.
INSERT `</body>` INTO TABLE html_lines.
INSERT `</html>` INTO TABLE html_lines.
DATA(html) = concat_lines_of(
table = html_lines
sep = cl_abap_char_utilities=>newline ).
server->response->set_content_type( content_type_html ).
server->response->set_cdata( html ).
ENDMETHOD.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Private Method ZCL_ZTEST01_WEB_MAINT->HANDLE_LIST
* +-------------------------------------------------------------------------------------------------+
* | [--->] SERVER TYPE REF TO IF_HTTP_SERVER
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD handle_list.
DATA fields TYPE ty_field_list.
DATA keyword TYPE string.
DATA json TYPE string.
fields = get_fields( ).
keyword = server->request->get_form_field( `keyword` ).
json = `{"table":"` && me->escape_json_value( CONV string( maintained_table ) ) && `","fields":`.
me->append_json_fields(
EXPORTING
fields = fields
CHANGING
json = json ).
json = json && `,"rows":`.
me->append_json_rows(
EXPORTING
fields = fields
keyword = keyword
CHANGING
json = json ).
json = json && `}`.
write_json(
server = server
json = json ).
ENDMETHOD.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Private Method ZCL_ZTEST01_WEB_MAINT->HANDLE_SAVE
* +-------------------------------------------------------------------------------------------------+
* | [--->] SERVER TYPE REF TO IF_HTTP_SERVER
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD handle_save.
DATA method TYPE string.
DATA fields TYPE ty_field_list.
DATA values TYPE string_table.
DATA row_ref TYPE REF TO data.
FIELD-SYMBOLS <row> TYPE any.
FIELD-SYMBOLS <component> TYPE any.
method = server->request->get_method( ).
IF method <> `POST`.
me->write_error( server = server status_code = 405 reason = `Method Not Allowed` message = `Only POST is supported` ).
RETURN.
ENDIF.
fields = get_fields( ).
values = get_request_values( server = server fields = fields ).
CREATE DATA row_ref TYPE (maintained_table).
ASSIGN row_ref->* TO <row>.
LOOP AT fields INTO DATA(field).
READ TABLE values INTO DATA(value) INDEX sy-tabix.
ASSIGN COMPONENT field-name OF STRUCTURE <row> TO <component>.
IF sy-subrc = 0.
<component> = value.
ENDIF.
ENDLOOP.
MODIFY (maintained_table) FROM <row>.
IF sy-subrc <> 0.
ROLLBACK WORK.
me->write_error( server = server status_code = 500 reason = `Save Failed` message = `Unable to save row` ).
RETURN.
ENDIF.
COMMIT WORK.
me->write_json( server = server json = `{"status":"ok"}` ).
ENDMETHOD.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Private Method ZCL_ZTEST01_WEB_MAINT->HANDLE_DELETE
* +-------------------------------------------------------------------------------------------------+
* | [--->] SERVER TYPE REF TO IF_HTTP_SERVER
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD handle_delete.
DATA method TYPE string.
DATA fields TYPE ty_field_list.
DATA values TYPE string_table.
DATA row_ref TYPE REF TO data.
DATA has_key_value TYPE abap_bool.
FIELD-SYMBOLS <row> TYPE any.
FIELD-SYMBOLS <component> TYPE any.
method = server->request->get_method( ).
IF method <> `POST`.
me->write_error( server = server status_code = 405 reason = `Method Not Allowed` message = `Only POST is supported` ).
RETURN.
ENDIF.
fields = get_fields( ).
values = get_request_values( server = server fields = fields ).
CREATE DATA row_ref TYPE (maintained_table).
ASSIGN row_ref->* TO <row>.
LOOP AT fields INTO DATA(field) WHERE is_key = abap_true.
READ TABLE values INTO DATA(value) INDEX sy-tabix.
IF value IS INITIAL.
me->write_error( server = server status_code = 400 reason = `Bad Request` message = `Key field is required` ).
RETURN.
ENDIF.
ASSIGN COMPONENT field-name OF STRUCTURE <row> TO <component>.
IF sy-subrc = 0.
<component> = value.
has_key_value = abap_true.
ENDIF.
ENDLOOP.
IF has_key_value = abap_false.
me->write_error( server = server status_code = 400 reason = `Bad Request` message = `No key field found` ).
RETURN.
ENDIF.
DELETE (maintained_table) FROM <row>.
IF sy-subrc <> 0.
ROLLBACK WORK.
me->write_error( server = server status_code = 404 reason = `Not Found` message = `Row was not found` ).
RETURN.
ENDIF.
COMMIT WORK.
me->write_json( server = server json = `{"status":"ok"}` ).
ENDMETHOD.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Private Method ZCL_ZTEST01_WEB_MAINT->GET_FIELDS
* +-------------------------------------------------------------------------------------------------+
* | [<-()] RESULT TYPE TY_FIELD_LIST
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD get_fields.
SELECT d~fieldname,
d~keyflag,
d~position,
coalesce( t_lang~ddtext, t_en~ddtext, d~fieldname ) AS label
FROM dd03l AS d
LEFT OUTER JOIN dd04t AS t_lang
ON t_lang~rollname = d~rollname
AND t_lang~ddlanguage = @sy-langu
AND t_lang~as4local = 'A'
LEFT OUTER JOIN dd04t AS t_en
ON t_en~rollname = d~rollname
AND t_en~ddlanguage = 'E'
AND t_en~as4local = 'A'
WHERE d~tabname = @maintained_table
AND d~as4local = 'A'
AND d~fieldname <> 'MANDT'
AND d~fieldname <> '.INCLUDE'
ORDER BY d~position
INTO TABLE @DATA(ddic_fields).
LOOP AT ddic_fields INTO DATA(ddic_field).
INSERT VALUE ty_field(
name = ddic_field-fieldname
json_name = to_lower( ddic_field-fieldname )
label = ddic_field-label
is_key = xsdbool( ddic_field-keyflag = 'X' )
readonly = xsdbool( ddic_field-keyflag = 'X' ) ) INTO TABLE result.
ENDLOOP.
ENDMETHOD.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Private Method ZCL_ZTEST01_WEB_MAINT->GET_REQUEST_VALUES
* +-------------------------------------------------------------------------------------------------+
* | [--->] SERVER TYPE REF TO IF_HTTP_SERVER
* | [--->] FIELDS TYPE TY_FIELD_LIST
* | [<-()] RESULT TYPE STRING_TABLE
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD get_request_values.
DATA body TYPE string.
DATA name_with_quotes TYPE string.
DATA search_name TYPE string.
DATA offset TYPE i.
DATA value_start TYPE i.
DATA value_end TYPE i.
DATA value TYPE string.
body = server->request->get_cdata( ).
LOOP AT fields INTO DATA(field).
name_with_quotes = `"` && field-name && `"`.
FIND FIRST OCCURRENCE OF name_with_quotes IN body MATCH OFFSET offset.
IF sy-subrc <> 0.
INSERT `` INTO TABLE result.
CONTINUE.
ENDIF.
search_name = body+offset.
FIND FIRST OCCURRENCE OF `:` IN search_name MATCH OFFSET offset.
IF sy-subrc <> 0.
INSERT `` INTO TABLE result.
CONTINUE.
ENDIF.
value_start = offset + 1.
search_name = search_name+value_start.
CONDENSE search_name NO-GAPS.
IF search_name(1) = `"`.
search_name = search_name+1.
FIND FIRST OCCURRENCE OF `"` IN search_name MATCH OFFSET value_end.
IF sy-subrc = 0.
value = search_name(value_end).
ELSE.
value = ``.
ENDIF.
ELSE.
FIND FIRST OCCURRENCE OF `,` IN search_name MATCH OFFSET value_end.
IF sy-subrc <> 0.
FIND FIRST OCCURRENCE OF `}` IN search_name MATCH OFFSET value_end.
ENDIF.
IF sy-subrc = 0.
value = search_name(value_end).
ELSE.
value = search_name.
ENDIF.
ENDIF.
INSERT value INTO TABLE result.
CLEAR value.
ENDLOOP.
ENDMETHOD.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Private Method ZCL_ZTEST01_WEB_MAINT->APPEND_JSON_FIELDS
* +-------------------------------------------------------------------------------------------------+
* | [--->] FIELDS TYPE TY_FIELD_LIST
* | [<-->] JSON TYPE STRING
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD append_json_fields.
json = json && `[`.
LOOP AT fields INTO DATA(field).
IF sy-tabix > 1.
json = json && `,`.
ENDIF.
json = json && `{"name":"` && me->escape_json_value( CONV string( field-name ) ) && `"`.
json = json && `,"json_name":"` && me->escape_json_value( field-json_name ) && `"`.
json = json && `,"label":"` && me->escape_json_value( field-label ) && `"`.
json = json && `,"is_key":` && COND string( WHEN field-is_key = abap_true THEN `true` ELSE `false` ).
json = json && `,"readonly":` && COND string( WHEN field-readonly = abap_true THEN `true` ELSE `false` ).
json = json && `}`.
ENDLOOP.
json = json && `]`.
ENDMETHOD.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Private Method ZCL_ZTEST01_WEB_MAINT->APPEND_JSON_ROWS
* +-------------------------------------------------------------------------------------------------+
* | [--->] FIELDS TYPE TY_FIELD_LIST
* | [--->] KEYWORD TYPE STRING
* | [<-->] JSON TYPE STRING
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD append_json_rows.
DATA table_ref TYPE REF TO data.
DATA appended_rows TYPE i.
FIELD-SYMBOLS <rows> TYPE STANDARD TABLE.
FIELD-SYMBOLS <row> TYPE any.
CREATE DATA table_ref TYPE STANDARD TABLE OF (maintained_table).
ASSIGN table_ref->* TO <rows>.
SELECT *
FROM (maintained_table)
ORDER BY PRIMARY KEY
INTO TABLE @<rows>
UP TO @max_rows ROWS.
json = json && `[`.
LOOP AT <rows> ASSIGNING <row>.
IF keyword IS NOT INITIAL AND me->row_matches_keyword( fields = fields row = <row> keyword = keyword ) = abap_false.
CONTINUE.
ENDIF.
IF appended_rows > 0.
json = json && `,`.
ENDIF.
me->append_json_row(
EXPORTING
fields = fields
row = <row>
CHANGING
json = json ).
appended_rows += 1.
IF appended_rows >= max_response_rows.
EXIT.
ENDIF.
ENDLOOP.
json = json && `]`.
ENDMETHOD.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Private Method ZCL_ZTEST01_WEB_MAINT->APPEND_JSON_ROW
* +-------------------------------------------------------------------------------------------------+
* | [--->] FIELDS TYPE TY_FIELD_LIST
* | [--->] ROW TYPE ANY
* | [<-->] JSON TYPE STRING
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD append_json_row.
FIELD-SYMBOLS <component> TYPE any.
DATA value TYPE string.
json = json && `{`.
LOOP AT fields INTO DATA(field).
IF sy-tabix > 1.
json = json && `,`.
ENDIF.
ASSIGN COMPONENT field-name OF STRUCTURE row TO <component>.
IF sy-subrc = 0.
value = <component>.
ELSE.
value = ``.
ENDIF.
json = json && `"` && me->escape_json_value( field-json_name ) && `":"` && me->escape_json_value( value ) && `"`.
ENDLOOP.
json = json && `}`.
ENDMETHOD.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Private Method ZCL_ZTEST01_WEB_MAINT->ROW_MATCHES_KEYWORD
* +-------------------------------------------------------------------------------------------------+
* | [--->] FIELDS TYPE TY_FIELD_LIST
* | [--->] ROW TYPE ANY
* | [--->] KEYWORD TYPE STRING
* | [<-()] RESULT TYPE ABAP_BOOL
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD row_matches_keyword.
FIELD-SYMBOLS <component> TYPE any.
DATA value TYPE string.
LOOP AT fields INTO DATA(field).
ASSIGN COMPONENT field-name OF STRUCTURE row TO <component>.
IF sy-subrc = 0.
value = <component>.
IF value CS keyword.
result = abap_true.
RETURN.
ENDIF.
ENDIF.
ENDLOOP.
result = abap_false.
ENDMETHOD.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Private Method ZCL_ZTEST01_WEB_MAINT->WRITE_JSON
* +-------------------------------------------------------------------------------------------------+
* | [--->] SERVER TYPE REF TO IF_HTTP_SERVER
* | [--->] JSON TYPE STRING
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD write_json.
server->response->set_content_type( content_type_json ).
server->response->set_cdata( json ).
ENDMETHOD.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Private Method ZCL_ZTEST01_WEB_MAINT->WRITE_ERROR
* +-------------------------------------------------------------------------------------------------+
* | [--->] SERVER TYPE REF TO IF_HTTP_SERVER
* | [--->] STATUS_CODE TYPE I
* | [--->] REASON TYPE STRING
* | [--->] MESSAGE TYPE STRING
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD write_error.
DATA json TYPE string.
server->response->set_status(
code = status_code
reason = reason ).
json = `{"status":"error","message":"` && me->escape_json_value( message ) && `"}`.
me->write_json(
server = server
json = json ).
ENDMETHOD.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Private Method ZCL_ZTEST01_WEB_MAINT->ESCAPE_JSON_VALUE
* +-------------------------------------------------------------------------------------------------+
* | [--->] VALUE TYPE STRING
* | [<-()] RESULT TYPE STRING
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD escape_json_value.
result = value.
REPLACE ALL OCCURRENCES OF `\` IN result WITH `\\`.
REPLACE ALL OCCURRENCES OF `"` IN result WITH `\"`.
REPLACE ALL OCCURRENCES OF cl_abap_char_utilities=>cr_lf IN result WITH `\n`.
REPLACE ALL OCCURRENCES OF cl_abap_char_utilities=>newline IN result WITH `\n`.
ENDMETHOD.
ENDCLASS.
最后在 SAP 里创建服务:
- 进入
SICF - 建服务节点,例如:/sap/bc/ztest01_maint
- Handler List 里填:ZCL_ZTEST01_WEB_MAINT
- 激活服务
- 如需免 SAP 用户登录,在该 SICF 节点配置匿名技术用户,并只给它
ZTEST01所需的最小权限