From 850d44ddb5b75631d4a154c555acd461840d9f53 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Thu, 30 Jul 2026 15:52:36 +0800 Subject: [PATCH] feat: add DimensionFilter widget with h_data_url, v_measure_url, show_data_url support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New Bricks widget that provides: - Search input bar with configurable placeholder - Horizontally scrollable dimension buttons with left/right arrows - Vertically scrollable measure buttons on the left side - Display area on the right side loading show_data_url with params - Single-selection for both horizontal and vertical dimensions - '全部' (All) option default selected for both axes - filter_changed event on every selection/search change - Registered via bricks.Factory.register('DimensionFilter', ...) --- bricks/build.sh | 1 + bricks/dimension_filter.js | 245 +++++++++++++++++++++++++++++++++++++ 2 files changed, 246 insertions(+) create mode 100644 bricks/dimension_filter.js diff --git a/bricks/build.sh b/bricks/build.sh index 001aad4..49b30e6 100755 --- a/bricks/build.sh +++ b/bricks/build.sh @@ -7,6 +7,7 @@ SOURCES=" page_data_loader.js factory.js uitypesdef.js utils.js uitype.js \ tree.js multiple_state_image.js dynamiccolumn.js form.js message.js conform.js \ paging.js datagrid.js iframe.js cols.js echartsext.js \ floaticonbar.js miniform.js wterm.js dynamicaccordion.js \ + dimension_filter.js \ binstreaming.js streaming_audio.js vadtext.js rtc.js docxviewer.js \ llm.js websocket.js datarow.js tabular.js continueaudio.js \ line.js pie.js bar.js gobang.js period.js iconbarpage.js \ diff --git a/bricks/dimension_filter.js b/bricks/dimension_filter.js new file mode 100644 index 0000000..65e1b91 --- /dev/null +++ b/bricks/dimension_filter.js @@ -0,0 +1,245 @@ +var bricks = window.bricks || {}; + +bricks.DimensionFilter = class extends bricks.VBox { + /* + options: + { + h_data_url: URL to fetch horizontal dimension labels/values, + v_measure_url: URL to fetch vertical dimension labels/values, + show_data_url: base URL to fetch display data, + search_placeholder: placeholder text for search input + } + events: + filter_changed: {h_id: string, v_id: string, keyword: string} + */ + constructor(opts){ + opts.width = '100%'; + opts.height = '100%'; + opts.overflow = 'hidden'; + super(opts); + this.selected_h_id = 'all'; + this.selected_v_id = 'all'; + this.search_keyword = ''; + this.h_buttons = []; + this.v_buttons = []; + this.h_data = []; + this.v_data = []; + schedule_once(this.build_all.bind(this), 0.1); + } + + async build_all(){ + this.build_search_bar(); + this.build_horizontal_bar(); + this.build_content_area(); + await this.load_dimensions_data(); + } + + build_search_bar(){ + this.searchbar = new bricks.SearchBar({ + placeholder: this.opts.search_placeholder || '搜索...', + width: '100%', + search_label: this.opts.search_label || '搜索', + clear_label: this.opts.clear_label || '清除' + }); + this.searchbar.bind('search', this.search_handle.bind(this)); + this.add_widget(this.searchbar); + } + + build_horizontal_bar(){ + var bar = new bricks.HBox({width: '100%', height: 'auto', flexShrink: 0}); + bar.set_style('alignItems', 'center'); + + this.left_arrow = new bricks.Button({ + label: '◀', + width: 'auto', + height: 'auto', + flexShrink: 0, + nonepack: true + }); + this.left_arrow.bind('click', this.scroll_left.bind(this)); + bar.add_widget(this.left_arrow); + + this.h_scroll = new bricks.HBox({ + width: '100%', + flexGrow: 1, + overflow: 'hidden' + }); + bar.add_widget(this.h_scroll); + + this.right_arrow = new bricks.Button({ + label: '▶', + width: 'auto', + height: 'auto', + flexShrink: 0, + nonepack: true + }); + this.right_arrow.bind('click', this.scroll_right.bind(this)); + bar.add_widget(this.right_arrow); + + this.add_widget(bar); + } + + build_content_area(){ + var content = new bricks.HBox({width: '100%', height: '100%'}); + + /* Left: vertical measure buttons in a scroll panel */ + this.v_scroll = new bricks.VScrollPanel({width: 'auto', height: '100%'}); + this.v_scroll.set_style('maxWidth', '200px'); + this.v_scroll.set_style('minWidth', '80px'); + content.add_widget(this.v_scroll); + + /* Right: display area for show_data_url results */ + this.display_filler = new bricks.Filler({flexGrow: 1}); + this.display_w = new bricks.VBox({width: '100%', height: '100%'}); + this.display_filler.add_widget(this.display_w); + content.add_widget(this.display_filler); + + this.add_widget(content); + } + + scroll_left(){ + var el = this.h_scroll.dom_element; + el.scrollLeft -= 150; + } + + scroll_right(){ + var el = this.h_scroll.dom_element; + el.scrollLeft += 150; + } + + async load_dimensions_data(){ + var jc = new bricks.HttpJson(); + try { + var hd = await jc.get(this.opts.h_data_url); + this.h_data = (hd && hd.rows) ? hd.rows : []; + } catch(e) { + console.error('DimensionFilter: failed to load h_data_url', e); + } + try { + var vd = await jc.get(this.opts.v_measure_url); + this.v_data = (vd && vd.rows) ? vd.rows : []; + } catch(e) { + console.error('DimensionFilter: failed to load v_measure_url', e); + } + this.build_horizontal_buttons(); + this.build_vertical_buttons(); + } + + build_horizontal_buttons(){ + this.h_scroll.clear_widgets(); + this.h_buttons = []; + + var all_btn = this._make_dim_btn('全部', 'all', true); + all_btn._is_h = true; + this.h_scroll.add_widget(all_btn); + this.h_buttons.push(all_btn); + + for (var i = 0; i < this.h_data.length; i++){ + var dim = this.h_data[i]; + var label = dim.name || dim.label || dim.id || String(dim); + var id = dim.id || dim.name || label; + var btn = this._make_dim_btn(label, id, false); + btn._is_h = true; + this.h_scroll.add_widget(btn); + this.h_buttons.push(btn); + if (this.selected_h_id == id){ + this.select_h_button(btn); + } + } + } + + build_vertical_buttons(){ + this.v_scroll.clear_widgets(); + this.v_buttons = []; + + var all_btn = this._make_dim_btn('全部', 'all', true); + all_btn._is_h = false; + this.v_scroll.add_widget(all_btn); + this.v_buttons.push(all_btn); + + for (var i = 0; i < this.v_data.length; i++){ + var dim = this.v_data[i]; + var label = dim.name || dim.label || dim.id || String(dim); + var id = dim.id || dim.name || label; + var btn = this._make_dim_btn(label, id, false); + btn._is_h = false; + this.v_scroll.add_widget(btn); + this.v_buttons.push(btn); + if (this.selected_v_id == id){ + this.select_v_button(btn); + } + } + } + + _make_dim_btn(label, id, is_selected){ + var btn = new bricks.Button({ + label: label, + width: 'auto', + height: 'auto', + flexShrink: 0 + }); + btn.dim_id = id; + if (is_selected){ + btn.set_css('dimension-btn-active'); + } + btn.bind('click', this.dim_button_clicked.bind(this, btn)); + return btn; + } + + dim_button_clicked(btn){ + if (btn._is_h){ + this.selected_h_id = btn.dim_id; + this.select_h_button(btn); + } else { + this.selected_v_id = btn.dim_id; + this.select_v_button(btn); + } + this.refresh_display(); + } + + select_h_button(btn){ + for (var i = 0; i < this.h_buttons.length; i++){ + this.h_buttons[i].set_css('dimension-btn-active', true); + } + btn.set_css('dimension-btn-active'); + } + + select_v_button(btn){ + for (var i = 0; i < this.v_buttons.length; i++){ + this.v_buttons[i].set_css('dimension-btn-active', true); + } + btn.set_css('dimension-btn-active'); + } + + search_handle(event){ + var d = event.params || {}; + this.search_keyword = d.keyword || ''; + this.refresh_display(); + } + + async refresh_display(){ + this.display_w.clear_widgets(); + var params = { + h_id: this.selected_h_id, + v_id: this.selected_v_id + }; + if (this.search_keyword){ + params.keyword = this.search_keyword; + } + var desc = { + widgettype: 'urlwidget', + options: { + url: this.opts.show_data_url, + params: params, + method: this.opts.show_data_method || 'GET' + } + }; + var w = await bricks.widgetBuild(desc, this); + if (w){ + this.display_w.add_widget(w); + } + this.dispatch('filter_changed', params); + } +}; + +bricks.Factory.register('DimensionFilter', bricks.DimensionFilter);