Odoo客户端的展示区域大部分使用一个通用布局,顶部是一个控制面板,面板下方就是主要内容的展示区域。这个布局使用Layout
组件来实现,Layout
是一个通用组件,可以通过t-set-slot
指令向Layout
组件中插入特定内容。
t-set-slot
指令插入的内容实际上是插入到控制面板组件中。Layout
组件只接收默认插槽插入的内容(主要内容)Layout
的源码位于/web/static/src/search
。
组件应用场景
- 在视图中使用,例如
form、list、kanban...
- 在action中使用,例如
ReportAction
组件解析
xml
<t t-name="web.Layout" owl="1">
<!-- 如果在Dialog组件中使用Layout,t-set-slot插入的内容会被挂载(t-portal指令)到Dialog的底部 -->
<!-- 如果不是在Dialog中,会插入面板组件的control-panel-bottom-left-buttons -->
<t t-if="env.inDialog" t-portal="'#' + env.dialogId + ' .modal-footer'">
<t t-slot="layout-buttons"/>
</t>
<!-- 接收的控制面板组件,这个组件是动态组件,支持对控制面板的扩展 -->
<t t-component="components.ControlPanel" slots="controlPanelSlots" t-if="display.controlPanel" display="display.controlPanel"/>
<t t-component="components.Banner" t-if="display.banner"/>
<div t-ref="content" class="o_content" t-attf-class="{{props.className}}" t-att-class="{ o_component_with_search_panel: display.searchPanel }">
<t t-component="components.SearchPanel" t-if="display.searchPanel"/>
<!-- 默认插槽,主要内容展示区域 -->
<t t-slot="default" contentRef="contentRef" />
</div>
</t>
js
// ControlPanel默认使用"@web/search/control_panel/control_panel"中定义的面板组件
// 可以使用自定义的控制面板组件,比如form视图使用FormControlPanel
export function extractLayoutComponents(params) {
// 从对象中取出"ControlPanel", "SearchPanel", "Banner"对应的组件类
return pick(params, "ControlPanel", "SearchPanel", "Banner");
}
export class Layout extends Component {
setup() {
this.components = extractLayoutComponents(this.env.config);
this.contentRef = useRef("content");
}
get controlPanelSlots() {
// 通过t-set-slot向Layout组件插入的内容都会通过slots插入到控制面板组件
const slots = { ...this.props.slots };
// 注意这里Layout插入的layout-buttons,会被转换为control-panel-bottom-left-buttons
// 默认面板组件的模板中还有一个control-panel-bottom-left插槽,也可以接收一些按钮,例如list视图中,选择记录后会在面板底部左侧渲染一些新的按钮
slots["control-panel-bottom-left-buttons"] = slots["layout-buttons"];
delete slots["layout-buttons"];
delete slots.default;
return slots;
}
...
}
组件的使用
ReportAction
中使用Layout组件
xml
<t t-name="web.ReportAction" owl="1">
<div class="o_action">
<!-- 隐藏控制面板的右上与右下部分 -->
<Layout display="{ controlPanel: { 'top-right' : false, 'bottom-right': false } }">
<!-- 布局中没有插入layout-buttons,因此面板的control-panel-bottom-left-buttons插槽不会有内容 -->
<t t-set-slot="control-panel-bottom-left">
<button t-on-click="print" type="button" class="btn btn-primary" title="Print">Print</button>
</t>
<!-- 插入到默认插槽 -->
<iframe t-ref="iframe" t-on-load="onIframeLoaded" class="h-100 w-100" t-att-src="reportUrl" />
</Layout>
</div>
</t>
控制面板的布局
控制面板的扩展
控制面板是Layout
组件的重要组成部分,开发者可以扩展控制面板,实现特定的展示效果。
FormControlPanel
form
视图中对控制面板进行了扩展,为控制面板添加了面包屑导航、动作按钮、分页等功能
js
import { ControlPanel } from "@web/search/control_panel/control_panel";
export class FormControlPanel extends ControlPanel {}
// 使用新模板替换原模板
FormControlPanel.template = "web.FormControlPanel";
xml
<t t-name="web.FormControlPanel" owl="1">
<div class="o_control_panel" t-ref="root">
<div t-if="display['top']" class="o_cp_top" t-att-class="{ 'flex-wrap': env.isSmall }">
<div class="o_cp_top_left d-flex flex-grow-1 align-items-center" t-att-class="{ 'w-100': env.isSmall }">
<t t-if="display['top-left']">
<!-- 面板屑导航 -->
<t t-slot="control-panel-breadcrumb">
<t t-if="env.isSmall">
<t t-call="web.Breadcrumbs.Small" t-if="!env.config.noBreadcrumbs"/>
</t>
<t t-else="">
<!-- 这里使用的是子模板,子模板是内联在主模板中的,因此子模板也会接收到Layout通过slots插入到面板组件的内容 -->
<!-- "web.Breadcrumbs子模板中定义了插槽<t t-slot="control-panel-status-indicator" /> -->
<!-- 这也是为什么在FormView中使用了该插槽,面板中却找不到,这个插槽是在子模板中定义的 -->
<t t-call="web.Breadcrumbs" t-if="!env.config.noBreadcrumbs"/>
</t>
</t>
</t>
</div>
<div class="o_cp_bottom_right w-auto flex-shrink-0 justify-content-between align-items-center"
t-att-class="{ 'flex-grow-1' : env.isSmall }">
<t t-if="env.isSmall">
<!-- 移动端生效 -->
<t t-slot="control-panel-status-indicator" />
</t>
<!-- 动作 -->
<t t-slot="control-panel-action-menu" t-if="display['bottom-left']"/>
<div t-if="pagerProps and pagerProps.total > 0" class="o_cp_pager" role="search">
<Pager t-props="pagerProps"/>
</div>
<!-- 新建 -->
<t t-slot="control-panel-create-button" />
</div>
</div>
</div>
</t>
在form
视图的模板中向扩展后的控制面板插入内容
- 这里通过
t-set-slot
插入到Layout
组件的内容都会通过slots
转插到面板中 - 只有
Render
会插入到Layout
的默认插槽
xml
<t t-name="web.FormView" owl="1">
<div t-att-class="className" t-ref="root">
<div class="o_form_view_container">
<Layout className="model.useSampleModel ? 'o_view_sample_data' : ''" display="display">
<!-- 插入到form view dialog 不会插入到form的控制面板 -->
<t t-set-slot="layout-buttons">
<t t-if="footerArchInfo and env.inDialog">
<t t-component="props.Renderer" record="model.root" Compiler="props.Compiler" archInfo="footerArchInfo" enableViewButtons.bind="enableButtons" disableViewButtons.bind="disableButtons"/>
</t>
<t t-else="">
<t t-call="{{ props.buttonTemplate }}"/>
</t>
</t>
<t t-set-slot="control-panel-action-menu">
<t t-if="props.info.actionMenus">
<ActionMenus ... />
</t>
</t>
<!-- control-panel-status-indicator对应的内容会插入到子模板 web.Breadcrumbs -->
<t t-set-slot="control-panel-status-indicator">
<t t-if="canEdit">
<FormStatusIndicator model="model" discard.bind="discard" save.bind="saveButtonClicked" isDisabled="state.isDisabled" fieldIsDirty="state.fieldIsDirty" />
</t>
</t>
<t t-set-slot="control-panel-create-button">
<t t-if="canCreate">
<button type="button" class="btn btn-outline-primary o_form_button_create" data-hotkey="c" t-on-click.stop="create" t-att-disabled="state.isDisabled">New</button>
</t>
</t>
<t t-component="props.Renderer" record="model.root" Compiler="props.Compiler" archInfo="archInfo" setFieldAsDirty.bind="setFieldAsDirty" enableViewButtons.bind="enableButtons" disableViewButtons.bind="disableButtons" onNotebookPageChange.bind="onNotebookPageChange" activeNotebookPages="props.state and props.state.activeNotebookPages"/>
</Layout>
</div>
</div>
</t>