背景需求
最近在开发一个Odoo项目时,客户提出了一个特定的搜索需求:希望在列表页面中展示多个多选下拉框作为过滤条件。用户选中任意下拉选项时,列表需要实时查询并显示对应的结果。
这种设计相较于Odoo原生搜索更为直观,特别是当用户需要同时基于多个维度筛选数据时,操作更加便捷。
Odoo原生搜索的局限性
Odoo作为一款国际化的开源ERP系统,其搜索功能设计理念与国内用户的使用习惯存在一定差异:
- 搜索模式单一:默认采用"搜索框+预设过滤器"的模式
- 多条件过滤不够直观:需要点击过滤器图标,在弹出窗口中配置多个条件
- 用户体验差异:国外用户习惯文本搜索+条件组合,国内用户更习惯可视化的多选过滤
解决方案:自定义控件开发
面对这种需求差异,我们决定采用Odoo的自定义开发能力。Odoo提供了灵活的扩展机制,特别是基于QWeb模板引擎,我们可以通过以下方式实现自定义搜索控件:
- 自定义多选下拉框组件
- 集成到搜索面板
- 重写列表视图控制器
- 动态构建搜索条件
完整方案实现
1. 多选下拉框组件 (XML模板)
首先需要在XML文件中定义自定义下拉框控件视图(multi_select_widget.xml):- <?xml version="1.0" encoding="UTF-8"?>
- <templates xml:space="preserve">
- <t t-name="multi_select" owl="1">
-
-
-
- <t t-esc="props.placeholder || 'Select options'"/>
-
-
-
-
- 1" >
- 已选择<t t-esc="state.selected.size"></t>个<t t-esc="props.fieldName"/>
-
-
-
-
- <t t-foreach="props.options" t-as="option" t-key="option">
- this.selectOption(option, ev)">
- <t t-esc="option"/>
-
- </t>
-
-
-
- </t>
- </templates>
复制代码 2. 多选下拉框组件逻辑 (JavaScript)
业务逻辑我们用js来实现(multi_select_widget.js)- import { Component, useState, useRef, onMounted, onWillUnmount } from "@odoo/owl";
- export class MultiSelectField extends Component {
- static template = "multi_select";
- static props = {
- options: Array,
- placeholder: { type: String, optional: true },
- fieldName: String,
- onChange: Function,
- };
- setup() {
- this.dropdownRef = useRef("multi_select_dropdown");
- this.state = useState({
- isOpen: false,
- selected: new Set(),
- });
- this.clickOutsideHandler = null;
- this.keydownHandler = null;
- onMounted(() => {
- this.setupEventListeners();
- });
- onWillUnmount(() => {
- this.cleanupEventListeners();
- });
- }
- toggleDropdown() {
- this.state.isOpen = !this.state.isOpen;
- }
- selectOption = (option, ev) => {
- if (this.state.selected.has(option)) {
- this.state.selected.delete(option);
- } else {
- this.state.selected.add(option);
- }
- this.props.onChange(this.props.fieldName, [...this.state.selected]);
- }
- setupEventListeners() {
- this.clickOutsideHandler = (event) => {
- if (!this.dropdownRef || !this.dropdownRef.el) return;
- if (!this.dropdownRef.el.contains(event.target)) {
- this.state.isOpen = false;
- }
- }
- this.keydownHandler = (event) => {
- if (event.key === 'Escape' && this.state.isOpen) {
- event.preventDefault();
- event.stopPropagation();
- event.stopImmediatePropagation();
- this.state.isOpen = false;
- }
- }
- document.addEventListener('mousedown', this.clickOutsideHandler, true);
- document.addEventListener('touchstart', this.clickOutsideHandler, true);
- document.addEventListener('keydown', this.keydownHandler, true);
- }
- cleanupEventListeners() {
- if (this.clickOutsideHandler) {
- document.removeEventListener('mousedown', this.clickOutsideHandler, true);
- document.removeEventListener('touchstart', this.clickOutsideHandler, true);
- }
- if (this.keydownHandler) {
- document.removeEventListener('keydown', this.keydownHandler, true);
- }
- this.clickOutsideHandler = null;
- this.keydownHandler = null;
- }
- }
复制代码 3.自定义搜索面板 (XML模板)
同样定义一个xml(search_widget.xml)- <?xml version="1.0" encoding="UTF-8"?>
- <templates xml:space="preserve">
- <t t-name="custom_search_panel" owl="1">
-
-
- <t t-if="state.loading">
-
- <i ></i>
- 正在加载数据...
-
- </t>
-
- <t t-if="state.error">
-
- <i ></i>
-
-
- </t>
-
- <t t-if="!state.loading and !state.error">
-
-
- <MultiSelectField
- fieldName="field_a"
- options="state.dropdownData.field_a"
- placeholder="'字段A筛选'"
- onChange="(field, values) => handleSelection(field, values)"
- />
-
- <MultiSelectField
- fieldName="field_b"
- options="state.dropdownData.field_b"
- placeholder="'字段B筛选'"
- onChange="(field, values) => handleSelection(field, values)"
- />
-
- <MultiSelectField
- fieldName="field_c"
- options="state.dropdownData.field_c"
- placeholder="'字段C筛选'"
- onChange="(field, values) => handleSelection(field, values)"
- />
-
- </t>
-
-
-
- </t>
- </templates>
复制代码 4.搜索面板业务逻辑 (JavaScript)
search_widget.js- import { Component, useState, onWillStart } from "@odoo/owl";
- import { registry } from "@web/core/registry";
- import { useService } from "@web/core/utils/hooks";
- import { MultiSelectField } from "./multi_select_widget";
- export class CustomSearchPanel extends Component {
- static template = "custom_search_panel";
- static components = { MultiSelectField };
- setup() {
- // 获取服务
- this.ormService = useService("orm");
-
- // 初始化响应式状态
- this.state = useState({
- dropdownData: {
- field_a: [],
- field_b: [],
- field_c: [],
- },
- selectedValues: {
- field_a: [],
- field_b: [],
- field_c: [],
- },
- loading: false,
- error: null,
- });
- // 组件挂载前加载数据
- onWillStart(async () => {
- await this.loadDropdownData();
- });
- }
- // 加载下拉框数据
- loadDropdownData = async () => {
- this.state.loading = true;
- this.state.error = null;
-
- try {
- // 调用后端方法获取下拉框数据
- const dropdownData = await this.ormService.call(
- "your.model.name", // 替换为实际模型名
- "get_filter_dropdown_data", // 后端方法名
- [],
- {}
- );
-
- this.state.dropdownData = dropdownData;
- } catch (error) {
- console.error("加载下拉框数据失败:", error);
- this.state.error = "加载筛选数据失败,请稍后重试";
- } finally {
- this.state.loading = false;
- }
- }
- // 处理选择变化
- handleSelection = async (fieldName, selectedValues) => {
- // 更新选中值
- this.state.selectedValues[fieldName] = selectedValues;
-
- // 生成搜索条件
- const domain = this.generateSearchDomain();
-
- // 触发搜索更新
- this.triggerSearchUpdate(domain);
- }
- // 生成搜索条件
- generateSearchDomain() {
- const domain = [];
-
- Object.entries(this.state.selectedValues).forEach(([field, values]) => {
- if (values && values.length > 0) {
- // 使用 'in' 操作符支持多选
- domain.push([field, 'in', values]);
- }
- });
-
- return domain;
- }
- // 触发搜索更新
- triggerSearchUpdate(domain) {
- // 更新搜索模型
- this.env.searchModel.updateDomain(domain);
-
- // 发送自定义事件通知列表刷新
- this.env.bus.trigger('custom_search:updated', {
- domain,
- timestamp: Date.now()
- });
- }
- }
- // 注册组件
- registry.category("view_components").add("custom_search_panel", CustomSearchPanel);
复制代码 5.自定义列表控制器 (JavaScript)
- import { registry } from "@web/core/registry";
- import { listView } from "@web/views/list/list_view";
- import { ListController } from "@web/views/list/list_controller";
- import { CustomSearchPanel } from "./search_widget";
- import { useBus } from "@web/core/utils/hooks";
- // 扩展原生列表控制器
- export class CustomListController extends ListController {
- static components = {
- ...ListController.components,
- SearchPanel: CustomSearchPanel, // 替换搜索组件
- };
-
- static template = "web.ListView";
- setup() {
- super.setup();
-
- // 监听自定义搜索事件
- useBus(this.env.bus, "custom_search:updated", (ev) => {
- this.handleCustomSearch(ev.detail.domain);
- });
- }
- // 处理自定义搜索
- async handleCustomSearch(domain) {
- try {
- // 显示加载状态
- this.model.isLoading = true;
- this.render();
-
- // 加载数据
- await this.model.load({ domain });
-
- // 更新分页信息
- if (this.model.data) {
- this.model.pager.limit = this.model.data.length;
- }
- } catch (error) {
- console.error("搜索数据失败:", error);
- } finally {
- this.model.isLoading = false;
- this.render();
- }
- }
- }
- // 注册自定义列表视图
- registry.category("views").add("custom_multi_select_list", {
- ...listView,
- Controller: CustomListController,
- display: {
- controlPanel: {
- 'bottom-left': false,
- 'bottom-right': false,
- },
- },
- });
复制代码 6.后端数据接口 (Python)
- # models/your_model.py
- from odoo import models, fields, api
- class YourModel(models.Model):
- _name = 'your.model.name'
- _description = '示例模型'
-
- # 定义字段
- field_a = fields.Selection([
- ('option1', '选项1'),
- ('option2', '选项2'),
- ('option3', '选项3'),
- ], string='字段A')
-
- field_b = fields.Char(string='字段B')
- field_c = fields.Many2one('related.model', string='字段C')
-
- # 获取下拉框数据的方法
- @api.model
- def get_filter_dropdown_data(self):
- """返回所有下拉框的选项数据"""
- return {
- 'field_a': self._get_field_a_options(),
- 'field_b': self._get_field_b_options(),
- 'field_c': self._get_field_c_options(),
- }
-
- def _get_field_a_options(self):
- """获取字段A的选项"""
- return [
- display_value
- for value, display_value in self._fields['field_a'].selection
- ]
-
- def _get_field_b_options(self):
- """获取字段B的去重值"""
- records = self.search_read(
- [('field_b', '!=', False)],
- ['field_b'],
- limit=100
- )
- return sorted(list(set([
- record['field_b']
- for record in records
- if record['field_b']
- ])))
-
- def _get_field_c_options(self):
- """获取字段C的关联选项"""
- related_records = self.env['related.model'].search_read(
- [],
- ['name'],
- limit=50
- )
- return [record['name'] for record in related_records]
复制代码 7. 视图配置 (XML)
- <?xml version="1.0" encoding="UTF-8"?>
- <odoo>
-
-
- <record id="view_custom_list" model="ir.ui.view">
- <field name="name">your.model.custom.list</field>
- <field name="model">your.model.name</field>
- <field name="arch" type="xml">
- <list js_>
- <field name="name" string="名称"/>
- <field name="field_a" string="字段A"/>
- <field name="field_b" string="字段B"/>
- <field name="field_c" string="字段C"/>
-
- </list>
- </field>
- </record>
- </odoo>
复制代码 来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |