news 2026/3/10 15:23:48

在Odoo18中实现多选下拉框搜索功能

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
在Odoo18中实现多选下拉框搜索功能

背景需求

最近在开发一个Odoo项目时,客户提出了一个特定的搜索需求:希望在列表页面中展示多个多选下拉框作为过滤条件。用户选中任意下拉选项时,列表需要实时查询并显示对应的结果。

这种设计相较于Odoo原生搜索更为直观,特别是当用户需要同时基于多个维度筛选数据时,操作更加便捷。

Odoo原生搜索的局限性

Odoo作为一款国际化的开源ERP系统,其搜索功能设计理念与国内用户的使用习惯存在一定差异:

  • 搜索模式单一:默认采用"搜索框+预设过滤器"的模式
  • 多条件过滤不够直观:需要点击过滤器图标,在弹出窗口中配置多个条件
  • 用户体验差异:国外用户习惯文本搜索+条件组合,国内用户更习惯可视化的多选过滤

解决方案:自定义控件开发

面对这种需求差异,我们决定采用Odoo的自定义开发能力。Odoo提供了灵活的扩展机制,特别是基于QWeb模板引擎,我们可以通过以下方式实现自定义搜索控件:

  1. 自定义多选下拉框组件
  2. 集成到搜索面板
  3. 重写列表视图控制器
  4. 动态构建搜索条件

完整方案实现

1. 多选下拉框组件 (XML模板)

首先需要在XML文件中定义自定义下拉框控件视图(multi_select_widget.xml):

/* by yours.tools - online tools website : yours.tools/zh/requestmethod.html */ <?xml version="1.0" encoding="UTF-8"?> <templates xml:space="preserve"> <t t-name="multi_select" owl="1"> <div class="multiselect-container" t-ref="multi_select_dropdown"> <div class="form-control" t-on-click="toggleDropdown"> <span t-if="state.selected.size === 0"> <t t-esc="props.placeholder || 'Select options'"/> </span> <div t-if="state.selected.size === 1" class="selected-options" > <span class="badge bg-primary me-1" t-esc="[...state.selected][0]"/> </div> <div t-if="state.selected.size > 1" class="selected-options" > <span class="badge bg-primary me-1">已选择<t t-esc="state.selected.size"></t>个<t t-esc="props.fieldName"/></span> </div> </div> <div t-if="state.isOpen" class="dropdown-menu show"> <t t-foreach="props.options" t-as="option" t-key="option"> <a href="#" class="dropdown-item" t-att-class="{'active': state.selected.has(option)}" t-on-click="(ev) => this.selectOption(option, ev)"> <t t-esc="option"/> </a> </t> </div> <style> .multiselect-container{ margin: 3px; width: 200px; } </style> </div> </t> </templates>

2. 多选下拉框组件逻辑 (JavaScript)

业务逻辑我们用js来实现(multi_select_widget.js)

/* by yours.tools - online tools website : yours.tools/zh/requestmethod.html */ 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"> <div class="custom-search-panel" t-att-data-loading="state.loading"> <!-- 加载状态 --> <t t-if="state.loading"> <div class="loading-state text-center p-3"> <i class="fa fa-spinner fa-spin me-2"></i> <span>正在加载数据...</span> </div> </t> <!-- 错误状态 --> <t t-if="state.error"> <div class="error-state alert alert-warning m-3"> <i class="fa fa-exclamation-triangle me-2"></i> <span t-esc="state.error"></span> </div> </t> <!-- 正常状态 --> <t t-if="!state.loading and !state.error"> <div class="search-filters-container"> <!-- 多选下拉框组件 --> <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)" /> </div> </t> <style> .custom-search-panel { padding: 16px; background: #f8f9fa; border-bottom: 1px solid #dee2e6; } .search-filters-container { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; } .loading-state { color: #6c757d; } .error-state { max-width: 600px; margin: 0 auto; } </style> </div> </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_class="custom_multi_select_list"> <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>
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/28 22:02:33

Open-AutoGLM强势领跑多模态榜单,TOP 1背后的5大核心技术曝光

第一章&#xff1a;Open-AutoGLM 多模态理解能力行业排名Open-AutoGLM 作为新一代开源多模态大模型&#xff0c;在多项权威基准测试中展现出卓越的跨模态理解能力&#xff0c;尤其在图文匹配、视觉问答和跨模态检索任务中表现突出。其基于大规模图文对预训练&#xff0c;并融合…

作者头像 李华
网站建设 2026/3/8 18:20:40

告别HDR播放尴尬:Downkyi视频格式转换全攻略

你是否曾经下载过精美的HDR视频&#xff0c;却在普通显示器上看到一片惨白&#xff1f;或者在手机上分享视频时&#xff0c;发现色彩完全失真&#xff1f;别担心&#xff0c;今天我将为你揭秘Downkyi的视频格式转换功能&#xff0c;让你彻底告别这些播放难题&#xff01; 【免费…

作者头像 李华
网站建设 2026/3/6 14:35:16

GHelper:终极轻量级ROG笔记本性能调校工具

GHelper&#xff1a;终极轻量级ROG笔记本性能调校工具 【免费下载链接】g-helper Lightweight Armoury Crate alternative for Asus laptops. Control tool for ROG Zephyrus G14, G15, G16, M16, Flow X13, Flow X16, TUF, Strix, Scar and other models 项目地址: https://…

作者头像 李华
网站建设 2026/3/3 19:51:52

低代码如何引爆AI生产力?Open-AutoGLM集成方案深度解析

第一章&#xff1a;低代码如何引爆AI生产力&#xff1f;在人工智能技术快速普及的今天&#xff0c;低代码平台正成为推动AI应用落地的核心引擎。通过可视化界面与模块化组件&#xff0c;开发者无需编写大量代码即可构建复杂的AI驱动应用&#xff0c;显著缩短开发周期并降低技术…

作者头像 李华
网站建设 2026/3/5 9:03:17

Python中的数据序列其二

目录 前言 一、字典 1、为什么需要字典(dict) 2、Python中字典(dict)的概念 3.字典的增删改查操作 增操作&#xff08;重点&#xff09; 删操作 改操作 查操作 综合案例 二、集合 1.什么是集合 2.集合的定义 3.集合操作的相关方法&#xff08;增删查&#xff09; 增操作 删操作…

作者头像 李华
网站建设 2026/3/4 3:24:55

Unity翻译插件重构指南:从零打造专业级本地化方案

Unity翻译插件重构指南&#xff1a;从零打造专业级本地化方案 【免费下载链接】XUnity.AutoTranslator 项目地址: https://gitcode.com/gh_mirrors/xu/XUnity.AutoTranslator 在游戏全球化浪潮中&#xff0c;Unity翻译插件已成为连接不同语言玩家的关键技术桥梁。本文将…

作者头像 李华