首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Geejing WebBuilder 组件实战:Wb.Window 弹窗,从表单布局到新增编辑数据闭环

Geejing WebBuilder 组件实战:Wb.Window 弹窗,从表单布局到新增编辑数据闭环

原创
作者头像
技术挖掘官
发布于 2026-11-13 11:02:38
发布于 2026-11-13 11:02:38
310
举报

页面上点「新增」,弹出一个表单,填完点确定,表格里多一行;点「编辑」,同一个弹窗再次出现,字段已经填好了——这是几乎所有业务系统每天都在跑的交互。很多框架把这件事拆成路由、状态管理、弹窗组件、表单库、请求层五块,新手要凑齐五份知识才能跑通。在 Geejing WebBuilder 里,它收敛成一个组件加一个回调:Wb.Window 负责把表单装起来、把值收齐、把按钮和校验管好,业务代码只需要回答「拿到这些值以后做什么」。

这篇文章只讲这一个组件,从最小可用配置开始,一路走到新增、编辑、关闭、复用和排错。所有配置名、方法名、事件名都取自平台自带的组件定义与官方示例,不依赖任何第三方库。对一套企业级快速开发平台来说,把高频交互收敛到一条可预期的链路上,比提供一百个零散配置项更有价值。

一、先认清角色:弹窗是一个会收值的容器

很多人第一次用弹窗,会把它理解成「盖在页面上的一层 div」。这个理解会直接导致后面踩坑:不知道字段值从哪里来,不知道确定按钮为什么能自动校验,也不知道为什么关掉以后数据还在。

在平台的组件清单里,弹窗其实是一族:Wb.Window 是通用窗体,Wb.Dialog 偏向带确定/取消按钮的对话框,Wb.Drawer 是侧滑抽屉。三者属性高度同构,本文选 Wb.Window 作为主线,因为它既出现在官方的窗体示例中,也是官方 CRUD 示例里承载新增/编辑表单的那一个。组件定义里 Wb.Window 有 129 个属性、31 个事件,看上去吓人,但真正决定一次业务闭环的只有三组:

分组

关键项

作用

布局组

layout、autoGrid、width、height、items

决定表单字段怎么排、放几个

对话框组

dialog、resetDialog、clearDialog、buttons、modal

决定有没有确定/取消、遮罩、关闭行为

提交组

okHandler、beforeok、ok、Wb.getValue、Wb.verify

决定点确定时值怎么收、怎么交给业务

理解了这个分组,剩下的属性都是「按需微调」。Geejing WebBuilder 的组件设计有个明显特点:容器自己知道里面有哪些控件。所以你不需要给每个输入框写 v-model,也不需要手动拼 FormData——窗口会把子控件的值一次性收成一个普通对象交给你。

二、三行配置,先让弹窗显示出来

平台模块用 XWL 描述界面,本质是一段 YAML。一个最小窗体长这样:

代码语言:yaml
复制
- cls: Wb.Window
  properties:
    cid: editWin
    title: 人员信息
    width: 40em
    layout: grid2
  items:
  - cls: Wb.Text
    properties:
      cid: code
      text: 编码
  - cls: Wb.Text
    properties:
      cid: full_name
      text: 姓名

几个名词先解释清楚:

  • cls 是组件类名,Wb.Window 就是窗体;
  • cid 是组件在当前模块里的唯一标识,之后所有 app.editWin 都靠它取到这个对象;
  • items 是子组件数组,输入框、下拉框、日期框都挂在这里;
  • layout: grid2 表示两列网格布局,autoGrid: 'true' 可以让字段自动依次入格;
  • 布尔值在 XWL 里写作字符串 'true',这是模块解析器的约定,照写即可。

窗口默认不会自己弹出来。官方示例的写法非常直白,在按钮的 click 事件里调一次 show():

代码语言:js
复制
app.modalWin.show();

如果希望模块一加载就可见,可以设置 visible: 'true';如果用的是 showDo() 这类带回调的快捷方法,又想自己控制显示时机,manualShow 可以阻止自动 show()。这三者的区别,是新手最容易混淆的第一处:窗口是常驻对象,show() 只是把它显示出来,hide() 只是藏起来,对象本身还在内存里。

三、把一个真实表单装进去

官方 CRUD 示例里的编辑窗体,是把「布局 + 校验 + 数据绑定」一次配齐的范本。下面这段是它的结构节选:

代码语言:yaml
复制
- cls: Wb.Window
  properties:
    cid: editWin
    autoGrid: 'true'
    layout: grid2
    resetDialog: 'true'
  items:
  - cls: Wb.Text
    properties:
      cid: code
      maxLength: '5'
      required: 'true'
      text: Code
  - cls: Wb.Select
    properties:
      bindField: user_id
      cid: user_name
      forceSelect: 'true'
      required: 'true'
      text: Username
      textField: user_name
      valueField: sid
      url: '@xurl+ ''/actions&xaction=selectUsers'''
  - cls: Wb.Number
    properties:
      cid: salary
      decimalCount: '2'
      text: Salary
  - cls: Wb.TextArea
    properties:
      cid: cv
      gridColumn: span 2
      height: 8em

值得逐条拆开看:

required: 'true' 把校验规则写在控件上,而不是写在提交按钮的 if 里;maxLength、decimalCount、valueType: email 同理。窗口点确定时会统一调用 Wb.verify() 检查窗内所有控件,不通过就停在原地,业务回调根本不会执行。这就是「弹窗自己会校验」的真相。

gridColumn: span 2、gridRow: span 3 用来让某个字段跨列或跨行,配合 autoGrid 可以省掉手写一个表格布局。url + valueField + textField 让下拉框直接从服务端取数,bindField 决定提交时用哪个字段名。这些都不是弹窗的能力,而是子控件自带的能力,窗口只负责把它们组织到一起。

作为一套企业级快速开发平台,这种「容器只做组织、控件自带校验与取数」的分工,让同一个窗体在新增、编辑、查看三种场景下都能复用,不需要复制三份界面定义。

四、核心契约:okHandler 拿到的是整窗的值

先回答最关键的问题:确定按钮被点击时,框架到底做了什么?组件源码里的 onOk 只有几行,但顺序非常重要(下面为节选,注释是解读):

代码语言:js
复制
onOk(e) {
  let me = this;
  e.stopEvent();
  if (Wb.verify(me)) {                              // ① 窗内控件全部校验通过
    if (me.fireEvent('beforeok') !== false) {       // ② beforeok 可一票否决
      me.okHandler?.call(me, me, Wb.getValue(me));  // ③ 收齐值,交给业务回调
      me.fireEvent('ok');                           // ④ 触发 ok 事件
    }
  }
}

也就是说,okHandler(win, values) 里的 values 不是某个输入框的值,而是 Wb.getValue(win) 收集的整窗对象,键就是各控件的 cid。字段没有 cid、或者压根没放进 items,它就不会出现在这个对象里——这是「提交后少一个字段」最常见的原因。

官方新增按钮的完整写法如下,可以直接照着改 URL:

代码语言:js
复制
let win = app.editWin;

win.icon = 'add';
win.title = Str.add;
win.focusControl = null;
win.okHandler = (win, values) => {
  Wb.ajax({
    url: xurl + '/actions&xaction=add',
    params: values,
    json: true,
    success(resp) {
      app.grid1.appendRecord(Wb.apply(values, resp));
      win.hide();
    },
    failure(resp) {
      Wb.checkExists(resp, 'wb_staff_code', app.editWin.down('code'));
    }
  });
};
win.show();
win.resetScroll();

三个细节决定了这段代码是否好用:

  1. win.hide() 必须写在 success 里。 请求失败时窗口保持打开,用户改一下就能重提;如果写在外面,请求还没回来弹窗就消失了,错误也无从提示。
  2. Wb.apply(values, resp) 把服务端回填合并进来。 主键、创建时间这类由数据库生成的值,合并后一起 appendRecord 追加到表格,不必重新查整张表。
  3. Wb.checkExists 处理唯一键冲突。 它把服务端的重复提示定位到 down('code') 这个控件上,用户能立刻看见是哪一格填重了。

failure 里没有关窗动作,这是刻意的。Geejing WebBuilder 把「什么时候关」的决定权留给业务,而不是替你乐观地关掉。

五、编辑复用同一个窗口:赋值、差异提交、自动重置

新增和编辑共用 editWin,靠的是打开前的四步准备。官方编辑按钮的完整实现(略去附件预览)如下:

代码语言:js
复制
let win = app.editWin, rec = app.grid1.selection, data;

if (!rec) {
  Wb.tipSelect();
  return;
}
data = rec.data;
win.icon = 'edit';
win.title = Str.edit + ' - ' + data.code;
win.focusControl = true;
Wb.setValue(win, data);
win.okHandler = (win, values) => {
  Wb.ajax({
    url: xurl + '/actions&xaction=edit',
    params: rec.getDiffer(values),
    json: true,
    success(resp) {
      rec.update(Wb.apply(values, resp));
      win.hide();
    },
    failure(resp) {
      Wb.checkExists(resp, 'wb_staff_code', app.editWin.down('code'));
    }
  });
};
win.show();

对照新增,差异只有四处,每一处都有讲究:

  • if (!rec) { Wb.tipSelect(); return; } 先挡住「没选行就点编辑」;
  • Wb.setValue(win, data) 把选中行的数据一次性灌进窗内所有控件,字段名与 cid 对应;
  • rec.getDiffer(values) 只提交被改动过的字段,避免把整行原样发回服务端,也减少并发覆盖;
  • focusControl 新增时设 null、编辑时设 true:前者让焦点不落在某个具体输入框,后者让焦点回到用户上次操作过的控件,连续编辑多行时手感差别很明显。

还有一条隐藏规则:okHandler 会在窗口隐藏时被清空。组件文档写得很直白——「It will be cleared after the window is hidden each time」,源码里对应 me.okHandler = undefined;。所以每次 show() 之前都要重新赋值,不能指望配一次管到底。这不是 bug,而是防止上一次提交的闭包被误触发。

resetDialog: 'true' 则负责另一个常见问题:编辑完关掉,再点新增,窗里还留着上一个人的姓名。它在 hide 事件上挂了 Wb.reset(this),自动把控件恢复到初始值;如果希望清空而不是恢复默认值,用 clearDialog: 'true'(对应 Wb.clear(this));只想要按钮和遮罩、不关心重置,用 dialog: 'true'。源码里三者的初始化逻辑是同一段:

代码语言:js
复制
if (me.dialog || resetDialog || clearDialog) {
  configs.modal ??= true;
  configs.buttons ??= [{ text: Str.ok, cid: 'ok', type: 'primary', handler: me.onOk, scope: me },
    { text: Str.cancel, cid: 'cancel', handler: me.onCancel, scope: me }];
  me.closeOnEsc ??= configs.closable;
  me.okOnEnter ??= true;
  if (clearDialog) me.on('hide', function () { Wb.clear(this); });
  else if (resetDialog) me.on('hide', function () { Wb.reset(this); });
}
if (me.okOnEnter) me.onEnter ??= me.onOk;
if (me.closeOnEsc) me.onEsc ??= me.onCancel;

换句话说,只要打开这三个开关之一,遮罩、确定/取消按钮、Esc 关闭、回车确定四件事就一次性配好了,不用手写 buttons。企业级快速开发平台常见的「约定优于配置」,在这里落成了几行确定性的代码。

六、关闭不是销毁:生命周期与多开窗口

close() 执行的是 this[this.closeAction]()。通用组件默认 closeAction: 'destroy',而 Wb.Window 特意把默认值改成了 'hide'——因为业务弹窗要反复复用,每次销毁重建既浪费又容易丢状态。

于是有两条路线:

  • 复用型(绝大多数表单):保持默认 hide,配 resetDialog,一个 editWin 用到底;
  • 一次性型:设 closeAction: 'destroy',关闭即释放;官方示例里那个「Destroy on Close」窗体就是这么配的,并在 destroy 事件里提示已释放。

如果一个按钮要开出多个互不干扰的窗口,官方示例给了完整的动态创建写法:

代码语言:js
复制
let win = app.dynamicNewWin;

delete win.cid;                 // 避免与实例 cid 冲突
win.owner = parentContainer;    // 父容器销毁时自动销毁该窗口
win.x = Wb.random(70) + 'vw';
win.y = Wb.random(70) + 'vh';
win = Wb.create(win);
win.show();

这里 delete win.cid 是关键:模板上的 cid 用于设计期定位,实例化多个副本时必须删掉,否则后开的会覆盖先开的引用;owner 把窗口挂到父容器生命周期上,避免页面切走后残留一堆孤儿窗口;instanced: 'false' 则表明这份定义是模板而非单例。

其他常用开关:modal: 'true' 加遮罩并限制焦点外溢;closeOnEsc、closeOnBack、okOnEnter 管键盘与返回键;beforeclose 可以在关闭前做「有未保存内容」的拦截;maximizable、resizable、autoCenter 属于外观微调。Geejing WebBuilder 把这些都放在同一个属性面板里,改一个值即可生效,不需要额外的状态变量。这类细节通常不会写进需求文档,却直接决定系统长期运行后页面是否越用越卡;一套企业级快速开发平台的功力,往往就藏在这些默认约定里。

七、两个快捷方法:把整套流程缩成一行

显式写法适合讲清原理,日常开发还可以更省。官方示例在注释里标了「Preferred way」,指的是表格组件提供的 insertRecord 与 editRecord:

代码语言:js
复制
// 新增:传请求配置 + 窗体,其余交给框架
app.grid1.insertRecord({
  url: xurl + '/actions&xaction=add',
  failure(resp) {
    Wb.checkExists(resp, 'wb_staff_code', app.editWin.down('code'));
  }
}, app.editWin);

// 编辑:同样只需请求配置 + 窗体
app.grid1.editRecord({
  url: xurl + '/actions&xaction=edit',
  failure(resp) {
    Wb.checkExists(resp, 'wb_staff_code', app.editWin.down('code'));
  }
}, app.editWin);

它们并不是黑盒。看实现就知道,insertRecord 内部调 win.showAdd(...),editRecord 内部调 win.showEdit(data, ...),而这两个方法最终都走 showDo(title, callback):

代码语言:js
复制
showAdd(subTitle, callback) {
  let me = this, scrollEl = me.scrollEl;
  if (Wb.isFunction(subTitle)) { callback = subTitle; subTitle = null; }
  me.icon = 'add';
  me.focusControl = null;
  me.showDo(subTitle ? (Str.add + ' - ' + subTitle) : Str.add, callback);
  scrollEl.scrollTop = scrollEl.scrollLeft = 0;
}

showEdit(data, subTitle, callback) {
  let me = this;
  if (Wb.isFunction(subTitle)) { callback = subTitle; subTitle = null; }
  me.icon = 'edit';
  Wb.setValue(me, data);
  me.focusControl = true;
  me.showDo(subTitle ? (Str.edit + ' - ' + subTitle) : Str.edit, callback);
}

showDo(title, callback) {
  let me = this;
  if (Wb.isFunction(title)) { callback = title; title = null; }
  if (title != null) me.title = title;
  if (callback) me.okHandler = callback;
  if (!me.manualShow) me.show();
}

上一节里手工做的「改图标、改标题、设焦点、灌数据、挂回调、显示、回滚滚动条」,这里全被包进去了;insertRecord 还会在请求成功后自动 addRecord、win.hide(),editRecord 自动 getDiffer、rec.update、win.hide()。明白显式写法之后再切到快捷方法,出问题时能立刻定位到是哪一层,不会卡在「框架替我做了什么」上。对一套企业级快速开发平台来说,同时保留展开写法与收敛写法,比只给一种抽象更实用。

八、顺带一提:消息类弹窗走同一套按钮机制

删除确认不需要自己搭窗体,直接调用封装好的静态方法:

代码语言:js
复制
Wb.confirm(Wb.getActionHint(recs, 'code'), f => {
  Wb.ajax({
    url: xurl + '/actions&xaction=del',
    params: { del: grid.originData },
    success() { grid.delRecords(); }
  });
});

需要自定义按钮时,用 Wb.Window.showMessage:

代码语言:js
复制
Wb.Window.showMessage({
  title: 'Customize', text: 'Some message', icon: 'share',
  name: 'sys.dialog',                 // 同名窗口复用同一个实例
  buttons: [
    { text: 'Primary', type: 'primary', cid: 'primary' },  // 第一个按钮 = 回车默认
    { text: Str.cancel, cid: 'cancel' }                     // 最后一个按钮 = Esc 默认
  ],
  callback(btn) {
    this.close();
    Wb.tip(btn + ' clicked');
  }
});

Wb.info、Wb.error、Wb.tip、Wb.choose 属于同一族,返回值和 callback 的用法一致。它们与表单弹窗共享 buttons、onEnter、onEsc 这套机制,所以学会一次配置,两种场景都能覆盖。Geejing WebBuilder 在官方文档里把「第一个按钮对应回车、最后一个对应 Esc」写进了注释,这类约定能省掉大量键盘事件处理代码。

九、排错速查表与自查清单

现象

原因

处理

点确定完全没反应,回调不进

Wb.verify 未通过,或 beforeok 返回了 false

先看必填项与格式校验,再检查 beforeok 的返回值

第一次能提交,第二次点确定没反应

okHandler 在 hide 时被清空

每次 show() 前重新赋值,或改用 showDo / showAdd / showEdit

请求还没回来弹窗就关了

win.hide() 写在了 Wb.ajax 外面

移到 success 回调内部,failure 保持窗口打开

提交对象少字段

控件没设 cid,或没放进窗口 items

补齐 cid,确认层级在窗口之下

新增时残留上一次编辑的值

没开 resetDialog,或用了不重置的 dialog

需要恢复默认值用 resetDialog,需要清空用 clearDialog

编辑把整行都发回服务端

直接提交了 values

改用 rec.getDiffer(values)

连续点新增后窗口互相覆盖

模板 cid 未删除,或按单例方式配置

动态多开参考 delete win.cid + Wb.create,并设 instanced: 'false'

关掉再开,滚动条停在中间

打开编辑窗时未回滚滚动位置

showAdd 会自动回滚;手工写法补 win.resetScroll()

页面切换后窗口对象还在

closeAction 默认是 hide

一次性窗口设 closeAction: 'destroy',或指定 owner 托管

最后给一份动手前的自查清单,照着过一遍基本不会返工:

  1. 窗口有唯一 cid,所有需要提交的控件也都有 cid;
  2. 表单布局确认 layout 与 autoGrid,跨列字段用 gridColumn;
  3. 三个对话框开关按需求选一个,不要同时开;
  4. 每次打开前重新设置 okHandler,并在请求成功后才 hide();
  5. 编辑路径用 Wb.setValue 回显、getDiffer 提交;
  6. 复用窗口配 resetDialog,一次性窗口配 closeAction: 'destroy';
  7. 唯一键冲突交给 Wb.checkExists 定位到具体控件;
  8. 熟练之后再考虑把新增/编辑替换为 insertRecord / editRecord。

把弹窗这件事从头到尾走一遍会发现,真正需要业务开发者动脑的只有「提交到哪个地址」和「成功后怎么更新表格」两件事,其余的布局、校验、取值、按钮、键盘、遮罩、焦点、重置都由组件承担。这正是企业级快速开发平台应该提供的东西:不是替你写死业务,而是把每次都要重写一遍的那部分彻底拿走。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、先认清角色:弹窗是一个会收值的容器
  • 二、三行配置,先让弹窗显示出来
  • 三、把一个真实表单装进去
  • 四、核心契约:okHandler 拿到的是整窗的值
  • 五、编辑复用同一个窗口:赋值、差异提交、自动重置
  • 六、关闭不是销毁:生命周期与多开窗口
  • 七、两个快捷方法:把整套流程缩成一行
  • 八、顺带一提:消息类弹窗走同一套按钮机制
  • 九、排错速查表与自查清单
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档