常用业务场景组合
import { RenderAppContextScope, RenderButton, RenderDockPanel, RenderFieldPresenter, RenderFormPanel, RenderNumberInput, RenderStackPanel, RenderText, RenderTextBox, createAppContextKey,} from 'ds-ui'
本页把布局、组件、应用上下文、命令和性能规则放到典型业务页面里说明。组件手册回答“某个控件怎么用”,本页回答“复杂界面应该怎么组合”。
组合原则
复杂页面先拆职责,再选组件:
应用壳
菜单、导航、工作区、全局上下文
页面壳
页面标题、查询区、操作区、主内容、详情区
业务块
表单、表格、树、图表、编辑器、弹窗
基础控件
TextBox、Dropdown、Button、TreeView、GridView 等
推荐规则:
- 页面根负责分区,不负责绘制业务细节。
- 业务块内部自己管理滚动,避免外层和内层滚动互相抢事件。
- 页面级状态放页面或页面上下文,应用级上下文只放登录态、租户、全局服务。
- 按钮、菜单、快捷键尽量走命令系统,避免每个按钮重复写权限判断。
- 大数据组件必须明确虚拟化、增量更新或可视区绘制策略。
场景一:复杂工作台
典型工作台包含菜单、左侧模块导航、中央多标签页和右侧诊断或消息区:
RenderDockPanel appShell
top: MenuBar + 全局 Toolbar
left: NavigationMenu
right: Notification / Runtime Diagnostics / Inspector
fill: TabbedWorkspace 或 DockWorkspace
每个 tab 内部再独立组合页面:
Tab Page
RenderAppContextScope
RenderCommandScope
RenderStackPanel vertical
PageHeader
Query Form
CommandToolbar
GridView / Editor / MarkdownViewer flex=1
实现时不要让应用壳直接持有页面草稿、当前选中行或当前文书。应用壳只负责打开页面、关闭页面和提供全局上下文。页面关闭时,页面 scope 和页面命令一起释放。
一个最小的布局骨架:
const appShell = new RenderDockPanel() const topBar = new RenderStackPanel({ orientation: 'horizontal', spacing: 8, crossAxisAlignment: 'center',})topBar.addChild(new RenderText('业务工作台'), 1)topBar.addChild(new RenderButton({ label: '刷新', onClick: reload })) const navigation = new RenderStackPanel({ orientation: 'vertical', spacing: 8, crossAxisAlignment: 'stretch',})navigation.addChild(new RenderText('模块导航')) const pageHost = new RenderStackPanel({ orientation: 'vertical', spacing: 8, crossAxisAlignment: 'stretch',})pageHost.addChild(new RenderText('当前页面'))pageHost.addChild(new RenderText('主内容区'), 1) topBar.height = 48navigation.width = 240appShell.addChild(topBar, { dock: 'top' })appShell.addChild(navigation, { dock: 'left' })appShell.setFill(pageHost)
场景二:查询 + 表格 + 详情
这是后台系统、CIS 字典维护、医嘱查询、模板列表最常见的页面模式。
RenderStackPanel vertical
PageHeader
RenderFormPanel 查询条件
RenderCommandToolbar
RenderDockPanel body
fill: GridView
right: Detail Panel / ObjectInspector / Form
关键点:
- 查询条件用 表单开发 的规则组织,不要手写坐标。
- 表格使用 GridView,由表格自己管理滚动、选择、排序和复制。
- 详情区可以是只读详情、编辑表单或对象查看器。详情区宽度固定或可拖拽,不要让 GridView 被详情内容撑开。
- 查询按钮、重置按钮、导出按钮走 命令与权限,便于按钮权限和快捷键统一管理。
数据流建议:
queryDraft
-> 点击查询
-> service.search(queryDraft)
-> rows
-> GridView
-> selectedRow
-> Detail Panel
不要把 rows 放到应用级上下文。它通常属于当前页面,页面关闭后应释放。
场景三:表单编辑页
表单页不要从一开始就做成“一个巨大表单”。复杂业务表单应分组:
RenderStackPanel vertical
PageHeader
RenderScrollViewer
RenderStackPanel vertical
Section 基本信息
RenderFormPanel
Section 联系方式
RenderFormPanel
Section 扩展信息
RenderFormPanel / GridView / TextArea
Footer CommandToolbar
字段层级:
RenderFormPanel
RenderFieldPresenter
RenderTextBox / RenderNumberInput / RenderComboBox / RenderDatePicker
表单草稿和字段绑定示例:
interface OrderDraft { name: string quantity: number} const orderDraft: OrderDraft = { name: '', quantity: 1,} const orderForm = new RenderFormPanel({ columns: 2, labelWidth: 96, adaptive: true, columnGap: 16, rowGap: 8,}) orderForm.addField(new RenderFieldPresenter({ label: '名称', required: true, child: new RenderTextBox({ value: orderDraft.name, placeholder: '请输入名称', onChange: value => { orderDraft.name = value }, }),})) orderForm.addField(new RenderFieldPresenter({ label: '数量', unit: '件', child: new RenderNumberInput({ value: orderDraft.quantity, min: 1, onChange: value => { orderDraft.quantity = value }, }),}))
校验建议:
- 即时校验更新字段的
status和message。 - 提交校验先收集所有错误,再滚动到第一个错误。
- 异步校验必须和页面生命周期绑定,页面关闭后不能继续更新 UI。
不要把后端原始对象直接双向绑定到控件。建议复制成页面 draft,保存成功后再合并回业务状态。
场景四:主从维护页
主从页通常是左侧树或列表,右侧详情编辑:
RenderDockPanel
left: TreeView / ListView
fill: StackPanel
Header
Detail Form
Child Grid
常见数据关系:
selectedMaster
-> detailDraft
-> childRows
-> 保存时一次提交主表和子表
交互建议:
- 左侧树或列表只负责选择,不直接修改右侧表单。
- 选择变化时,如果右侧有未保存草稿,先走关闭保护或切换确认。
- 子表使用 GridView;不要在 GridView 单元格内嵌完整表单。
- 当前主对象可以放页面上下文,供右侧表单、右键菜单和弹窗读取。
场景五:复杂上下文组合
复杂业务系统里,上下文按生命周期分层,而不是把所有东西放全局。
Application appContext
currentUser
tenant
permission service
dictionary service
Workspace / Tab / Page context
currentPatient
currentVisit
currentDocument
page draft
page service
Modal / Popover context
open reason
selected row
temporary draft
页面级注入示例:
const currentRecordKey = createAppContextKey<{ id: string; title: string }>('record.current') const recordContent = new RenderStackPanel({ orientation: 'vertical', spacing: 8,})recordContent.addChild(new RenderText('当前文书')) const recordPage = new RenderAppContextScope({ values: [ [currentRecordKey, { id: 'r001', title: '入院记录' }], ], child: recordContent,})
上下文放置规则:
| 对象 | 推荐范围 | 原因 |
|---|---|---|
| 登录用户、租户 | 应用级 | 整个应用共享,生命周期等同应用。 |
| 权限服务、字典服务 | 应用级 | 多页面复用,通常是无页面状态服务。 |
| 当前患者、当前文书 | 页面或 tab 级 | 不同 tab 可以打开不同患者或文书。 |
| 查询条件、选中行、编辑草稿 | 页面级 | 页面关闭后应释放,不能串到其他页面。 |
| 弹窗参数、临时草稿 | 浮层级 | 弹窗关闭后释放。 |
| 大数组、大文档、大图片缓存 | 页面或文档级 | 避免全局长期持有导致内存不下降。 |
场景六:弹窗编辑
弹窗适合短流程编辑,不适合承载完整复杂页面。
推荐结构:
打开源页面
当前上下文和 selectedRow
-> showModal / showPopover
local draft
form
confirm / cancel
-> onConfirm 返回结果
-> 页面更新 rows 或 detailDraft
设计规则:
- 弹窗打开时复制一份 draft,取消时直接丢弃。
- 确定时返回结果,由页面决定如何合并。
- 弹窗需要读取当前患者、当前文书时,要继承打开源页面上下文。
- 弹窗内异步请求、定时器和订阅必须在关闭时释放。
详见 弹窗编辑 和 Popup 与 Overlay。
场景七:带权限的操作区
业务按钮不要直接散落在页面里。推荐从命令定义生成工具栏、菜单和快捷键。
CommandManager
record.save
record.submit
record.delete
RenderCommandToolbar
根据 command state 绘制按钮
MenuBar / ContextMenu / Keyboard
复用同一批 command
权限和启用状态分工:
| 判断 | 说明 |
|---|---|
visible |
当前用户或当前页面是否应该看到这个命令。 |
enabled |
当前状态下按钮是否可点击。 |
canExecute |
执行前最终判断,避免状态变化导致误执行。 |
execute |
只写真正的业务动作,不重复写 UI 状态判断。 |
当表单 draft、选中行、页面状态变化时,调用命令管理器刷新状态。这样按钮、菜单和快捷键会一致变化。
场景八:大数据页面
大数据页面必须先确定“谁拥有滚动”:
| 页面类型 | 滚动所有者 |
|---|---|
| 大表格 | GridView 自己管理滚动。 |
| 大树 | TreeView 自己管理滚动和虚拟行。 |
| 大文档 | 文档编辑器或 MarkdownViewer 自己管理滚动。 |
| 普通表单 | 外层 ScrollViewer 管理滚动。 |
| 工作台 | 每个 tab 页面自己管理内部滚动。 |
常见错误:
- 外层 ScrollViewer 包住 GridView,又让 GridView 自己滚动。
- 把 10 万行数据转成 10 万个 RenderObject。
- hover 时重新构造整棵 render tree。
- 页面关闭后全局缓存仍持有 rows、图片或文档对象。
性能文档从 性能总览 开始阅读。