赣州培训网站开发,WordPress巨卡无比,网页设计基础教程第七章课后习题,wordpress外链背景需求
最近在开发一个Odoo项目时#xff0c;客户提出了一个特定的搜索需求#xff1a;希望在列表页面中展示多个多选下拉框作为过滤条件。用户选中任意下拉选项时#xff0c;列表需要实时查询并显示对应的结果。
这种设计相较于Odoo原生搜索更为直观#xff0c;特别是当…背景需求最近在开发一个Odoo项目时客户提出了一个特定的搜索需求希望在列表页面中展示多个多选下拉框作为过滤条件。用户选中任意下拉选项时列表需要实时查询并显示对应的结果。这种设计相较于Odoo原生搜索更为直观特别是当用户需要同时基于多个维度筛选数据时操作更加便捷。Odoo原生搜索的局限性Odoo作为一款国际化的开源ERP系统其搜索功能设计理念与国内用户的使用习惯存在一定差异搜索模式单一默认采用搜索框预设过滤器的模式多条件过滤不够直观需要点击过滤器图标在弹出窗口中配置多个条件用户体验差异国外用户习惯文本搜索条件组合国内用户更习惯可视化的多选过滤解决方案自定义控件开发面对这种需求差异我们决定采用Odoo的自定义开发能力。Odoo提供了灵活的扩展机制特别是基于QWeb模板引擎我们可以通过以下方式实现自定义搜索控件自定义多选下拉框组件集成到搜索面板重写列表视图控制器动态构建搜索条件完整方案实现1. 多选下拉框组件 (XML模板)首先需要在XML文件中定义自定义下拉框控件视图(multi_select_widget.xml)/* by yours.tools - online tools website : yours.tools/zh/requestmethod.html */ ?xml version1.0 encodingUTF-8? templates xml:spacepreserve t t-namemulti_select owl1 div classmultiselect-container t-refmulti_select_dropdown div classform-control t-on-clicktoggleDropdown span t-ifstate.selected.size 0 t t-escprops.placeholder || Select options/ /span div t-ifstate.selected.size 1 classselected-options span classbadge bg-primary me-1 t-esc[...state.selected][0]/ /div div t-ifstate.selected.size 1 classselected-options span classbadge bg-primary me-1已选择t t-escstate.selected.size/t个t t-escprops.fieldName//span /div /div div t-ifstate.isOpen classdropdown-menu show t t-foreachprops.options t-asoption t-keyoption a href# classdropdown-item t-att-class{active: state.selected.has(option)} t-on-click(ev) this.selectOption(option, ev) t t-escoption/ /a /t /div style .multiselect-container{ margin: 3px; width: 200px; } /style /div /t /templates2. 多选下拉框组件逻辑 (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 version1.0 encodingUTF-8? templates xml:spacepreserve t t-namecustom_search_panel owl1 div classcustom-search-panel t-att-data-loadingstate.loading !-- 加载状态 -- t t-ifstate.loading div classloading-state text-center p-3 i classfa fa-spinner fa-spin me-2/i span正在加载数据.../span /div /t !-- 错误状态 -- t t-ifstate.error div classerror-state alert alert-warning m-3 i classfa fa-exclamation-triangle me-2/i span t-escstate.error/span /div /t !-- 正常状态 -- t t-if!state.loading and !state.error div classsearch-filters-container !-- 多选下拉框组件 -- MultiSelectField fieldNamefield_a optionsstate.dropdownData.field_a placeholder字段A筛选 onChange(field, values) handleSelection(field, values) / MultiSelectField fieldNamefield_b optionsstate.dropdownData.field_b placeholder字段B筛选 onChange(field, values) handleSelection(field, values) / MultiSelectField fieldNamefield_c optionsstate.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 /templates4.搜索面板业务逻辑 (JavaScript)search_widget.jsimport { 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], limit100 ) 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], limit50 ) return [record[name] for record in related_records]7. 视图配置 (XML)?xml version1.0 encodingUTF-8? odoo !-- 自定义列表视图 -- record idview_custom_list modelir.ui.view field namenameyour.model.custom.list/field field namemodelyour.model.name/field field namearch typexml list js_classcustom_multi_select_list field namename string名称/ field namefield_a string字段A/ field namefield_b string字段B/ field namefield_c string字段C/ !-- 其他字段 -- /list /field /record /odoo