5 数据可视化与分析#

Oracle APEX 的原生区域组件可以把同一套应用数据呈现为报表、内容行、卡片、图表、地图、日历、树和完全自定义的动态内容。实际开发时,先判断用户要完成的是精确查看、自助分析、浏览入口、趋势比较、空间定位、时间安排、层级导航,还是特殊格式展示;再选择相应区域类型,配置数据源,并在属性编辑器(Property Editor)中微调行为和外观。

本章的共同操作原则是:只要区域 SQL、WHERE 条件或刷新逻辑引用页面项绑定变量,例如 :P1_SAL < :P1_COMM,就必须在区域的 Page Items to Submit 中列出这些页面项,例如 P1_SAL,P1_COMM。否则区域刷新时 APEX 服务器端拿不到浏览器中的最新值,绑定变量可能为 null,从而导致查询没有返回数据或显示结果不符合用户刚刚选择的条件。

学习建议:从 Gallery 安装 Sample Reports 应用,配合本章逐一观察不同区域类型的配置方式、运行时交互和属性编辑器中的关键开关。

5.1 使用经典报表列出数据#

Classic Report 区域以表格方式显示数据,并可选择启用分页。若结果集确定很小,可以一次显示所有行;若数据量可能较大,应配置分页,让 APEX 按页高效取数。经典报表适合开发者希望稳定控制列、行模板、分页和链接行为的页面。

在页面设计器(Page Designer)中调整列属性,可以控制显示类型、列标题、格式掩码、排序、对齐等行为。对于 Plain Text 类型列,可以用 HTML Expression 自定义显示内容,把 HTML 标签、条件格式指令和列值组合在一起;即便某些列被隐藏,也可以通过 #SOME_COLUMN_NAME# 引用它们。若 Deptno 这类列保存的是外键,列类型可设为 Plain Text (based on List of Values),让 APEX 自动显示列表值中的友好名称,而不是直接显示外键编号。

图 5-1 经典报表区域显示员工数据并启用分页。

Standard 报表模板是默认选择。可在 Universal Theme Reference App(oracleapex.com/ut)查看其他模板,例如 Value Attribute PairsMedia ListSearch ResultsTimeline。若内置模板仍无法满足需求,可以创建自定义模板,以所需方式格式化经典报表的行和列。

复现检查:新建或打开一个使用 EMP 数据的经典报表页;确认分页能按页加载;把外键列配置为基于 LOV 的纯文本;再用 HTML Expression 引用一个隐藏列,验证运行页显示的是格式化后的用户友好内容。

5.2 可个性化的交互式报表#

Interactive Report 是面向最终用户自助分析的表格区域。用户可以在开发者允许的范围内筛选、排序、突出显示、格式化、计算、聚合、分组、透视、绘图、下载和邮件发送报表。开发者需要控制每个交互式报表开放哪些功能,也可以在跳转到目标页面时传入动态过滤条件,让用户一进入页面就只看到与当前任务相关的数据。

官方示例中的 Employees 交互式报表展示了多个用户个性化能力:添加一个由 Salary 加 Commission 组成的计算列 Earnings;隐藏 Salary 和 Department 列;定义 Earnings 求和聚合;按 Earnings 从高到低排序;只显示部门 20 和 30 且薪资在 800 到 2000 之间的行;高亮 Earnings 大于 1500 的行;并按部门把结果分区显示。

图 5-2 交互式报表中的筛选、高亮、分组、计算列和聚合结果。

交互式报表列若为 Plain Text,同样可以使用 HTML Expression 自定义渲染,并通过 #SOME_COLUMN_NAME# 引用显示列或隐藏列。运行页面时,开发者可从 App Builder 保存主默认报表,设置默认列、列顺序、排序以及其他报表状态;也可保存一个或多个替代默认报表。替代默认报表会出现在页面设计器组件树的 Saved Reports 下,用户可在工具栏列表中选择。

最终用户也可以保存私人报表。私人报表与公共默认报表出现在同一个选择列表中,但只对创建它的用户可见。用户还可以按需邮件发送某个报表,或订阅周期性邮件。

复现检查:在运行页配置一次筛选、排序、计算列和聚合;从 App Builder 运行页面并保存为 Primary Default Report;再用普通最终用户账户保存一个私人报表,确认两个报表的可见范围不同。

5.3 灵活格式化并对数据执行操作#

Content Row 区域把每一行数据渲染成更适合浏览和移动端查看的行卡片。不同显示槽位都可以混合 HTML 标签、条件格式指令和列值,并用 &SOME_COLUMN_NAME. 语法引用列值,包括隐藏列。

官方示例在 Title 槽位使用 &ENAME.,在 Avatar Icon 槽位使用 &ICON.,在 Description 槽位使用 &JOB. in &DNAME. (&LOC.) since &HIREDATE.。Overline 槽位包含条件格式表达式:Compensation: &SAL. {if COMM/} + &COMM. commission = &TOTAL_COMP.{endif/}。因此,没有佣金的 FORD 和 SCOTT 会以不同方式显示。示例还定义了 Edit 和 Promote 等行级操作,并按页显示 5 个员工。

图 5-3 Content Row 区域使用分页、条件格式和行级操作菜单。

需要批量操作时,可以启用单选或多选行为,并配置隐藏页面项保存被选中行的主键。示例页面还使用 Order By Item,让最终用户在多个排序方式之间切换。

图 5-4 Content Row 区域启用多行选择,MARTIN、WARD 和 SCOTT 被选中。

复现检查:确认 SQL 查询包含所有槽位需要的列;为金额和日期列设置合适格式掩码;启用多选后检查隐藏页面项是否保存被选中主键;切换 Order By Item 后确认区域按新顺序刷新。

5.4 使用卡片呈现信息块#

若希望把结果显示为信息块,可使用 Cards 区域。Cards 区域提供多个卡片槽位,可以直接选择列,也可以配置 HTML 表达式,把 HTML 标签、条件格式指令和 &SOME_COLUMN_NAME. 列值语法组合起来。

官方示例中,Title 槽位使用 ENAME,图标列使用 ICON,Body 槽位使用 &JOB. in &DNAME. (&LOC.)<br> since &HIREDATE.,Secondary Body 槽位使用 Compensation: &SAL. {if COMM/} + &COMM. commission = &TOTAL_COMP.{endif/}。可以为整张卡片、标题、副标题或每张卡片上的按钮定义操作,让用户对任意一行执行动作。

图 5-5 使用 Cards 区域显示员工信息。

Cards 还可以显示来自 BLOB 列或 URL 的图片,并配置图片位置和尺寸。卡片包含媒体时,可以让点击图片触发操作;如果操作配置为按钮,它们会显示在卡片底部。官方示例展示了带员工头像、多个自定义格式槽位和 Promote 按钮的卡片网格。

图 5-6 Cards 区域显示头像媒体和每张卡片上的操作按钮。

复现检查:先用纯文本列完成卡片标题、正文和次要正文;再加入图标或媒体列;最后配置整卡链接或按钮操作,确认点击目标传递的是受保护的主键值。

5.5 用图表与仪表板快速理解信息#

Chart 区域用于以可视化方式展示数据。APEX 支持 18 类图表,并可在同一页面放置多个图表构成仪表板。多数图表类型都支持多个数据系列;少数图表类型只使用单一系列。

仪表板设计应从业务问题出发,而不是从图表外观出发。类别比较通常使用柱状或条形图;趋势使用折线或面积图;构成比例可用堆叠图、环形图或饼图;相关性用散点图或气泡图;项目计划可用甘特图。多个图表放在一页时,仍按普通区域一样配置栅格布局。

5.5.1 选择图表类型#

创建 Chart 区域时,在 Create Page 向导中选择图表类型。可选类型包括 Area、Bar、Box Plot、Bubble、Combination、Status Meter Gauge、Donut、Funnel、Gantt、Line、Line with Area、Pie、Polar、Pyramid、Radar、Range、Scatter 和 Stock。向导以缩略图展示每种类型,便于快速选择。

图 5-7 创建图表区域时可选择的原生图表类型。

复现检查:用同一份 EMP 汇总数据分别创建条形图、折线图和饼图,比较它们表达“部门薪资总额”时的理解成本,选择最贴合问题的类型。

5.5.2 定义图表系列#

图表系列(Series)通过表名、SQL 查询或其他数据源定义。Gantt、Status Meter Gauge 和 Stock 图表只有一个系列;其他类型通常可在同一张图中显示多个系列。

官方示例使用条形图按部门号显示员工薪资和佣金总和。若把系列配置为堆叠显示,部门 30 的佣金柱会叠放在薪资柱上,而不是并排显示。

图 5-8 按部门号汇总员工薪资与佣金,并以堆叠条形图显示。

在页面设计器中选择 Salaries 系列后,可在属性编辑器的 Column Mapping 中选择标签列、值列和聚合方式。官方示例中两个系列都使用 EMP 表作为数据源。

图 5-9 页面设计器中选择 Salaries 系列并配置列映射。

复现检查:为薪资和佣金分别建立系列;确认两个系列的标签维度一致;开启堆叠后检查同一部门的两项指标是否落在同一个类别下。

5.5.3 在图表中包含关联数据#

若图表应显示部门名称而不是部门号,可以把系列改为 SQL Query,通过 EMP 和 DEPT 表关联取得 DNAME。薪资系列可使用:

SELECT E.DEPTNO,
       D.DNAME,
       SUM(E.SAL) AS TOTAL_SALARIES
  FROM EMP E
  JOIN DEPT D ON D.DEPTNO = E.DEPTNO
 GROUP BY E.DEPTNO, D.DNAME

佣金系列类似,但 COMM 可能为空,因此需要用 NVL() 把空值当作 0 参与汇总:

select e.deptno,
       d.dname,
       sum(nvl(e.comm,0)) as total_commissions
  from emp e
  join dept d on d.deptno = e.deptno
 group by e.deptno, d.dname

调整 Salaries 和 Commissions 两个系列的 Column Mapping,使用 DNAME 作为标签,分别使用 TOTAL_SALARIESTOTAL_COMMISSIONS 作为值列,并按 DEPTNO 排序。这样 x 轴显示用户能理解的部门名称,同时排序仍保持业务顺序。

图 5-10 堆叠条形图改为显示关联得到的部门名称。

复现检查:确认两个系列都返回相同标签粒度;值列均为数值;空佣金不会让聚合结果为空;图表横轴显示部门名称而非部门号。

5.6 在地图上显示数据#

Map 区域用于显示与地球上具体位置或区域相关的数据。一个地图可以包含多个图层。每个图层都需要配置表、SQL 查询或其他数据源,并选择图层类型。

地图适合回答位置、距离、密度、路径和覆盖范围相关的问题。点图层可展示门店、员工住址或设备位置;线图层可展示路线或边界;多边形可展示行政区、销售区域或地块;热力图和拉伸多边形可把数量大小叠加到空间对象上。

5.6.1 选择地图图层类型#

地图可以显示多个图层。每个图层都要配置数据源并选择类型:用标记显示点,用线表示导航路径等路线,用多边形表示任意地理区域。Heat Map 图层把额外数值关联到每个点;Extruded Polygon 图层把额外数值关联到每个形状。

图 5-11 Create Page 向导中展示的五种地图图层类型。

空间数据可以是经度和纬度两列,适用于 Points 和 Heat Map 图层;所有图层类型也可以使用 SDO_GEOMETRY 或 GeoJSON。开发者可决定最终用户能看到哪些地图工具,例如缩放、测距和搜索半径。如果内置地图背景不满足需求,也可以定义自定义背景。大量点相互接近时,建议启用点聚类,让低缩放级别下的地图更清晰。还可以用动态操作响应地图变化或对象选择。

复现检查:创建 Map 页面并选择 Points 图层;验证经纬度列或几何列映射正确;开启聚类后在低缩放级别观察标记是否聚合。

5.6.2 添加点图层#

可以通过 EMP 与 EMP_ADDRESSES 表关联,创建一个显示员工住址点位的地图图层。数据源既可以配置在地图区域级别并被各图层引用,也可以由每个图层单独定义。为了便于维护,建议创建数据库视图封装区域 SQL,再在区域和图层中引用视图列。

create or replace view emp_with_addresses as
select e.empno,
       e.ename,
       a.address,
       a.latitude,
       a.longitude,
       e.sal + nvl(e.comm,0) as total_compensation
  from emp e
  join emp_addresses a on a.empno = e.empno;

地图使用该视图后,可把点位配置为 LATITUDELONGITUDE,并用 HTML Expression &ENAME. - &ADDRESS. 定义提示信息。官方示例显示旧金山和 Marin County 附近的员工地址点;鼠标悬停在 BLAKE 的标记上时,提示为 BLAKE - 1246 Palou Ave, San Francisco, CA 94124

图 5-12 员工地址以地图点位显示。

复现检查:确认视图返回经纬度、名称和地址;地图点位能显示;悬停提示能引用当前行的员工姓名和地址。

5.6.3 用热力图表达数量#

Heat Map 图层把一个额外数值关联到每个点,用来表达不同地图位置上的数量差异。官方示例在员工点图层之外增加一个 Salaries 热力图图层,并使用 TOTAL_COMPENSATION 作为值列。显示两个图层时,员工住址标记旁会出现与总薪酬相对大小成比例的圆。

图 5-13 地图同时显示点图层和薪酬热力图图层。

复现检查:在同一地图中保留员工地址 Points 图层,再添加 Heat Map 图层;将值列设为 TOTAL_COMPENSATION;验证圆的大小能体现相对数量。

5.6.4 简化点与距离计算#

SDO_GEOMETRY 类型可让应用存储、搜索并操作空间数据。点是最简单的空间对象,可表示地址和 GPS 位置;同一类型也能表示路线或边界所需的线、城市边界或销售区域等多边形,以及服务区、配送区或影响区等圆形。

在 Oracle AI Database 26ai 中,可以用简洁表达式创建点:SDO_GEOMETRY(longitude, latitude)。在更早数据库版本中,可用辅助函数简化点和距离计算。

function make_point(
    p_longitude in number,
    p_latitude  in number)
return sdo_geometry deterministic
is
    geom_type_2d_point constant number := 2001;
    spatial_ref_type_gps constant number := 4326;
begin
    return case
        when p_longitude is not null and p_latitude is not null then
            sdo_geometry(
                geom_type_2d_point,
                spatial_ref_type_gps,
                sdo_point_type(p_longitude, p_latitude, null),
                null,
                null)
    end;
end;
function distance_in_km(
    p_origin      in sdo_geometry,
    p_destination in sdo_geometry)
return number deterministic
as
begin
    return sdo_geom.sdo_distance(
        p_origin,
        p_destination,
        tol => 1,
        unit => 'unit=KM');
end distance_in_km;

提示:对任意 SDO_GEOMETRY 对象,可以使用 SDO_UTIL 包中的 TO_GEOJSON 函数取得 GeoJSON 表示。

复现检查:在数据库中创建辅助函数;用两组经纬度调用 make_pointdistance_in_km;验证返回单位为公里。

5.6.5 与参考点的距离#

通过数据库视图可以把点与点之间的距离计算封装起来,让页面配置更简单。假设办公室位于著名的 Pyramid Building,经度为 -122.402768,纬度为 37.795270。若要显示每位员工住址离办公室多少公里,可以在 EMP_WITH_ADDRESSES 视图中加入 DISTANCE_FROM_OFFICE_KM 列。

create or replace view emp_with_addresses as
select e.empno,
       e.ename,
       a.address,
       a.latitude,
       a.longitude,
       e.sal + nvl(e.comm,0) as total_compensation,
       round(distance_in_km(
           make_point(a.longitude,a.latitude),
           make_point(-122.402768,37.795270)),1) as distance_from_office_km
  from emp e
  join emp_addresses a on a.empno = e.empno

还可以增加第三个图层表示办公室位置,查询如下:

select 37.795270 as latitude,
       -122.402768 as longitude
  from dual

然后把 Employees 图层的提示 HTML 表达式改为 &ENAME. - &ADDRESS. (&DISTANCE_FROM_OFFICE_KM. km)。官方示例中,鼠标悬停在 SCOTT 的地图标记上时,提示显示她距离办公室 10.2 km。

图 5-14 地图显示办公室位置以及员工住址到办公室的距离。

复现检查:确认视图中距离列返回一位小数;办公室图层固定显示参考点;员工标记提示包含姓名、地址和距离。

5.7 在日历中展示事件#

Calendar 区域用于以月、周、日或列表视图显示排程事件。如果数据源只有事件标题和开始日期,事件显示在发生当天;如果启用 Show Time,事件会显示为一小时事件;如果同时配置结束日期,日历会显示事件持续时间。

5.7.1 配置基础日历#

添加 Calendar 区域后,需要配置它显示事件所需的关键属性。选择数据源后,至少设置事件 TitleStart DateEnd Date。官方示例假设存在 EMP_MEETINGS 表,用来记录一个员工与另一个员工之间的会议。表中 CALENDAR_FOR_EMPNO 表示发起邀请的员工,MEET_WITH_EMPNO 表示受邀员工。

图 5-15 EMP_MEETINGS 表记录 EMP 员工之间的排程事件。

日历区域基于 EMP_MEETINGS 表后,可把 PURPOSE 配置为事件标题,把 STARTS_AT 配置为开始日期,把 ENDS_AT 配置为结束日期。还可配置是否显示周末、允许月视图/周视图/日视图,以及是否显示用于切换当前月、周或日的导航按钮。

图 5-16 KING 员工日程中的四个会议,并由选择列表动态刷新。

页面右上角的 P19_CALENDAR_FOR 选择列表决定日历显示哪位员工的事件。区域 WHERE 条件可引用该页面项,既显示此员工发起的会议,也显示此员工受邀参加的会议:

CALENDAR_FOR_EMPNO = :P19_CALENDAR_FOR
OR MEET_WITH_EMPNO = :P19_CALENDAR_FOR

要让选择列表变化后自动刷新日历区域,创建一个响应选择列表 Change 事件的动态操作。并且必须在日历区域的 Page Items to Submit 中列出 P19_CALENDAR_FOR,让刷新使用浏览器中的最新选择值。

复现检查:切换 Calendar For 选择列表;确认日历立即刷新;检查区域提交项包含 P19_CALENDAR_FOR;确认 WHERE 条件同时包含发起人和参会人两种情况。

5.7.2 添加创建与编辑链接#

使用属性编辑器可以配置创建和编辑链接,把用户导航到合适页面来录入或修改事件。创建链接通过 Link Builder 对话框定义目标页面。配置后,用户可以点击日历空白日期,或点击并拖拽一段时间范围来创建新事件。

官方示例把创建链接配置为跳转到页面 20,并把当前页面项 P19_CALENDAR_FOR 的值传给目标页的 P20_CALENDAR_FOR_EMPNO。还可以使用 &NAME. 语法,把 APEX$NEW_START_DATEAPEX$NEW_END_DATE 传给事件创建页面。类似地,配置事件编辑目标链接后,用户点击现有事件即可打开详情编辑页。

图 5-17 配置创建链接,把开始和结束日期传给事件创建页。

复现检查:在日历空白位置创建事件;确认目标页面收到员工编号、开始日期和结束日期;点击已有事件时确认打开的是编辑页面而不是创建页面。

5.7.3 用拖放修改事件#

启用拖放支持后,用户可以把事件拖到新的开始日期或时间,也可以拖动事件上边缘或下边缘来改变持续时间。开发者只需要编写逻辑,使用新的开始和结束日期更新事件记录。

APEX 把开始和结束日期格式化为 12 位规范数字,例如 2025 年 4 月 19 日 9:30 表示为 202504190930。更新逻辑可把 APEX$NEW_START_DATEAPEX$NEW_END_DATE 和主键 APEX$PK_VALUE 作为绑定变量使用:

update emp_meetings
   set starts_at = to_date(:APEX$NEW_START_DATE, 'YYYYMMDDHH24MISS'),
       ends_at   = to_date(:APEX$NEW_END_DATE,   'YYYYMMDDHH24MISS')
 where id = :APEX$PK_VALUE;

复现检查:拖动一个事件到新时间;刷新页面后确认数据库中 STARTS_ATENDS_AT 已更新;补充业务校验,避免无权限用户或冲突日程被直接改动。

5.7.4 使用视图关联更多信息#

可以用数据库视图为日历事件补充关联信息。例如要在每个事件中显示参会员工姓名,可创建 EMP_MEETINGS_V,把 EMP_MEETINGSEMP 关联两次,分别取得邀请人姓名和受邀人姓名。

create or replace view emp_meetings_v as
select m.id,
       m.purpose,
       m.starts_at,
       m.ends_at,
       m.calendar_for_empno,
       cal_e.ename as calendar_for_ename,
       m.meet_with_empno,
       meet_e.ename as meet_with_ename
  from emp_meetings m
  join emp cal_e /* inviting emp */
    on cal_e.empno = m.calendar_for_empno
  join emp meet_e /* invited emp */
    on meet_e.empno = m.meet_with_empno

然后把日历区域改为基于以下 SQL 查询。查询中的 CASE 表达式根据当前 P19_CALENDAR_FOR 值,显示“与谁开会”的另一方姓名。

select id,
       purpose||' with '||
       case to_number(:P19_CALENDAR_FOR)
            when calendar_for_empno then meet_with_ename
            else calendar_for_ename
       end as formatted_title,
       starts_at,
       ends_at
  from emp_meetings_v
 where calendar_for_empno = :P19_CALENDAR_FOR
    or meet_with_empno = :P19_CALENDAR_FOR

把事件标题改为使用 FORMATTED_TITLE 后,日历会显示 KING 正在与哪位员工开会。

图 5-18 事件显示会议目的以及正在会面的员工姓名。

复现检查:视图能返回邀请人和受邀人姓名;选择不同员工时,事件标题始终显示“另一方”的名字;日历刷新仍提交 P19_CALENDAR_FOR

5.7.5 增强事件显示#

要进一步自定义日历事件显示,可以组合使用自定义格式记号和事件内容格式化函数。官方示例先在 SQL 中使用 APEX_STRING.FORMAT,把会议目的和“with 某人”的信息注入字符串模板 [[%s]]|with %s

select id,
       apex_string.format(
           '[[%s]]|with %s',
           purpose,
           case to_number(:P19_CALENDAR_FOR)
                when calendar_for_empno then meet_with_ename
                else calendar_for_ename
           end) as formatted_title,
       starts_at,
       ends_at
  from emp_meetings_v
 where calendar_for_empno = :P19_CALENDAR_FOR
    or meet_with_empno = :P19_CALENDAR_FOR

某条记录的格式化标题可能变成 [[Hiring Review]]|with ADAMS。之后在日历区域中使用 JavaScript Initialization Function 修改传入的 pOptions 对象并返回它。该函数可使用开源 FullCalendar 库的功能调整日历外观和行为。示例把 | 替换为 <br>,把 [[]] 替换为加粗标签,并把事件内容居中,同时设置 en-gb 区域格式。

function (pOptions) {
    pOptions.locale = "en-gb";
    pOptions.eventContent = function(arg) {
        return {
            html: '<center>' + arg.event.title.replace(/\|/g, '<br>')
                .replace(/\[\[/g,'<b>')
                .replace(/\]\]/g,'</b>')
                + '</center>'
        };
    };
    return pOptions;
}

不能直接在原始标题字符串中使用 HTML 标签,因为出于安全原因,APEX 引擎会转义它们,结果不会按预期渲染。用视图关联员工姓名,再用自定义事件标题格式化后,每个事件第一行以粗体显示会议目的,第二行显示参会对象。还可以继续使用 CSS 调整样式。建议安装 Gallery 中的 Sample Calendar 应用,研究更多 Calendar 区域示例。

图 5-19 KING 的一周会议日历,事件标题分行并加粗显示。

复现检查:确认 SQL 输出不含直接 HTML;JavaScript 初始化函数返回修改后的 pOptions;事件标题在运行页中分两行显示且第一行加粗。

5.8 使用树浏览层级数据#

Tree 区域用于显示层级数据,例如 EMP 表中的员工管理链。APEX 可以负责计算层级,开发者只需配置每个树节点要显示的数据以及节点之间的父子关系。

5.8.1 配置基础树显示#

选择 EMP 这类表后,至少要配置 Node Id 列、Node Label 列和 Parent Key 列。Parent Key 的值引用另一个节点 ID,用来形成层级关系。官方示例使用 ENAME 作为节点标签、EMPNO 作为节点 ID、MGR 作为父键。

示例中 Hierarchy 属性设为 Computed with SQL,表示由 APEX 引擎在运行时自动生成 SQL 计算层级。Order Siblings By 设为 ENAME,用于排序同级节点;Start Tree With 设为 Value is NULL,表示树的顶层节点来自父键 MGR 为空的行。

图 5-20 一个可工作的 Tree 区域所需的最小配置。

这些设置会生成管理层级树。KING 的 MGR 为空,因此显示为根节点。APEX 在运行时用配置列生成层级查询,一次性取回树的数据。示例中 CLARK 和 JONES 层级被展开,显示多个层级的直接下属。

图 5-21 基础 Tree 区域显示 EMP 管理层级。

复现检查:确认节点 ID 唯一;父键引用真实存在的节点或为空;根节点正确显示;展开管理层级时能看到下属。

5.8.2 跟踪并记住选择#

若要跟踪并记住 Tree 选择,需要配置一个隐藏页面项保存选中节点值,并把该页面项的 Value Protected 设为 false。如果页面渲染时该隐藏页面项已有值,Tree 会用它恢复选中节点。

先定义类似 P17_SELECTED_EMPNO 的隐藏项,并确认 Session State > StoragePer Session (Persistent)。然后在 Tree 区域中把 Node Value 设为 EMPNO,把 Selected Node Page Item 设为 P17_SELECTED_EMPNO,即可在隐藏项有值时恢复选择。

还需要两步补全运行时保存:先为 Tree 区域设置 Static ID,例如 emptree;再创建一个动态操作,监听 Tree 区域的 Selection Changed [Tree] 事件。动态操作第一步为 Set Value,影响元素为 P17_SELECTED_EMPNOFire on Initialization 设为 false,Set Type 使用 JavaScript Expression:

apex.region("emptree").call("getSelectedNodes")[0]?.id

如果只需要在用户提交页面时记住选择,这一步已经足够。若希望用户选择树节点后立即写入会话状态,再增加一个 Execute Server-side Code 动态操作步骤,并把 Page Items to Submit 设为 P17_SELECTED_EMPNO;PL/SQL 代码可以只是:

null; -- Push Items to Submit into Session State
图 5-22 动态操作把选中节点 ID 写入隐藏页面项。

复现检查:选择一个节点后刷新页面;确认同一会话内能恢复选择;若配置立即写入会话状态,检查服务器端会话状态中隐藏项值已更新。

5.8.3 给树添加图标#

可以用更有语义的节点图标向最终用户传达额外信息。若要控制每个节点的图标,在查询中提供一个额外列,列值为要使用的 Font APEX 图标名称。可在 Universal Theme Reference App 的 Icons 页面查看可用图标名称。

官方示例创建 EMP_WITH_ICONS_V 视图,用 CASE 根据员工 JOB 返回不同图标类名:

create or replace view emp_with_icons_v as
select empno,
       ename,
       job,
       deptno,
       mgr,
       hiredate,
       sal,
       comm,
       'fa '||
       case job
            when 'PRESIDENT' then 'fa-badgerine'
            when 'ANALYST'   then 'fa-line-chart'
            when 'CLERK'     then 'fa-user-headset'
            when 'SALESMAN'  then 'fa-badge-dollar'
            when 'MANAGER'   then 'fa-users'
       end as icon
  from emp

把 Tree 区域改为基于该视图,并将 Icon CSS Class Column 设为 ICON 后,每个节点会按员工岗位显示对应图标。

图 5-23 员工层级树根据 JOB 使用不同图标。

复现检查:确认图标列返回完整 CSS 类;树区域的 Icon CSS Class Column 指向该列;不同岗位节点显示不同图标。

5.8.4 在树中显示父子数据#

可以用 UNION ALL 查询并巧妙调整外键值,在 Tree 区域中显示父子数据。官方示例要显示部门节点及其下按字母顺序排列的员工。Tree 区域需要三个基础元素:节点标签、节点 ID 和父 ID。

在 EMP 这样的单表层级中,EMPNOMGR 都是员工编号。但若要组合部门编号和员工编号,必须确保两类节点 ID 不会冲突。示例通过把 DEPTNO 乘以 -1,让部门节点 ID 为负数,而员工编号保持正数。

图 5-24 使用 Tree 区域浏览部门与员工父子数据。
create or replace view dept_emp_tree_v as
select -1 * deptno as node_id,
       dname as node_label,
       null as parent_key,
       'fa fa-building-o' as icon
  from dept
union all
select empno as node_id,
       ename as node_label,
       -1 * deptno as parent_key,
       icon
  from emp_with_icons_v

随后将 Tree 区域配置为基于该 SQL 查询,并把命名清晰的列分别映射到 Node ID、Node Label 和 Parent Key。Hierarchy 使用 Computed in SQLStart Tree With 设为 Value is NULL,第一层即为部门节点,部门下方显示该部门员工。把 Order Siblings By 设为 NODE_LABEL 后,部门和员工节点都会按字母排序。

复现检查:部门节点 ID 与员工节点 ID 不冲突;部门节点父键为空;员工节点父键指向所属部门的负数 ID;同级节点按标签排序。

5.9 动态格式化数据#

当 APEX 其他区域类型都无法满足展示要求时,可以使用 Dynamic Content 区域以程序方式生成 HTML 标记。代码构造一个包含标签和数据的 CLOB,并返回给 APEX 引擎,由引擎把它放入页面对应位置。

动态内容提供最大自由度,也意味着开发者需要自己承担 HTML 结构、CSS、转义和刷新状态管理。若普通报表、卡片或图表能完成任务,应优先使用标准区域;只有确实需要完全控制标记时再使用 Dynamic Content。

5.9.1 生成 HTML 标签来呈现数据#

当需求要求完全控制 HTML 标记时,使用 Dynamic Content 区域。官方示例用一个复古的员工薪资 ASCII 图表说明 Dynamic Content 的基本工作方式。虽然 APEX Chart 区域会更精致,但这个例子能清楚展示如何用 PL/SQL 生成页面内容。

图 5-25 Dynamic Content 区域生成的 VT220 风格员工薪资图表。

把区域类型设为 Dynamic Content 后,编写返回 CLOB 的 PL/SQL 函数体。示例先查询最高薪资作为比例尺,再按薪资降序循环员工行。对每位员工,用当前薪资除以最高薪资得到比例,再乘以 40 并四舍五入,得到 40 字符宽条形图中应显示的块字符数量。随后使用 rpad() 生成条形字符串,把员工名、竖线和薪资放入 <pre> 预格式化标签。局部过程 p() 用来把每一行 HTML 逐步追加到 l_content

declare
    l_max_sal number;
    l_bar     varchar2(1000);
    l_blocks  number;
    l_content clob;

    procedure p(p_text varchar2) is
    begin
        l_content := l_content || p_text;
    end p;
begin
    select max(sal) into l_max_sal from emp;

    for j in (select ename, sal from emp order by sal desc) loop
        l_blocks := round(j.sal / l_max_sal * 40);
        l_bar := rpad('▒', l_blocks, '▒');
        p('<pre>' || rpad(j.ename, 10) || ' | ' || l_bar || ' ' || j.sal || '</pre>');
    end loop;

    return l_content;
end;

复现检查:Dynamic Content 区域返回 CLOB;运行页显示每个员工一行;最高薪资对应最长条;输出标签没有破坏页面结构。

5.9.2 使用 CSS 设置样式#

Dynamic Content 区域生成的 HTML 可以引用 CSS 类来微调样式。为了让结果看起来像 DEC VT220 终端,可在页面属性编辑器的 CSS > Inline 中定义名为 vt220 的 CSS 类。类名前带点,规则中设置黑色背景、绿色等宽字体、内边距、圆角、边框和发光阴影。为了覆盖 Universal Theme 对 <pre> 的默认样式,还可增加 .vt220 pre 规则收紧行距。

.vt220 {
    background-color: black;
    color: #00FF00;
    font-family: 'Courier New', monospace;
    font-size: 14px;
    padding: 1.5em;
    border-radius: 16px;
    border: 3px solid #333;
    box-shadow: 0 0 20px #00ff00aa;
    width: fit-content;
    margin: 2em auto;
}

.vt220 pre {
    line-height: 1.0;
}

要把 CSS 类应用到 Dynamic Content 区域,在区域的 Appearance > CSS Classes 属性中填写 vt220,不要加前导点。

复现检查:页面 Inline CSS 存在 .vt220.vt220 pre;区域 CSS Classes 填写 vt220;运行页显示绿色等宽字体和黑色背景。

5.9.3 引用页面项值#

和其他区域类型一样,可以用动态操作交互式刷新 Dynamic Content 区域。官方示例页面包含一个 P22_DEPARTMENT 选择列表,占布局栅格中的 3 列;同一行还有一个老式 ASCII 图表 Dynamic Content 区域。为选择列表创建响应 Change 事件的动态操作,动作步骤为 Refresh 动态内容区域。这样最终用户更改部门后,图表会按最新部门重新显示。

图 5-26 Dynamic Content 区域引用页面项值显示部门薪资图表。

实现时,需要调整 Dynamic Content 区域 PL/SQL 块中的两处查询,在最高薪资查询和员工循环查询中都引用 :P22_DEPARTMENT 绑定变量:

-- Adjusted two queries to reference page item value as bind variable
select max(sal)
  into l_max_sal
  from emp
 where deptno = :P22_DEPARTMENT;

for j in (
    select ename, sal
      from emp
     where deptno = :P22_DEPARTMENT
     order by sal desc
) loop
    l_blocks := round(j.sal / l_max_sal * 40);
    l_bar := rpad('▒', l_blocks, '▒');
    p('<pre>' || rpad(j.ename, 10) || ' | ' || l_bar || ' ' || j.sal || '</pre>');
end loop;

最后一步是在 Dynamic Content 区域的 Page Items to Submit 属性中列出 P22_DEPARTMENT。这样区域每次刷新时,APEX 引擎都会自动从浏览器取得该页面项的最新值。

复现检查:选择不同部门后区域刷新;查询只返回所选部门员工;区域 Page Items to Submit 包含 P22_DEPARTMENT;没有出现绑定变量为空导致的无数据。