feat(mobile): 手机端H5适配收编入框架——TabBar/ChipBar控件+声明式mobile_hidden/mobile_opts+TabPanel mobile_bar模式+AgentIO.set_pipeline切产线+index_tab.js动态tab收编(修show_tabcontent未定义name/refresh语义/add_tab不存在的add_removeable三处旧bug)+PopupWindow全屏化+iOS 16px输入防缩放(2026-09-07用户需求:适配逻辑不出bricks框架)

This commit is contained in:
yumoqing 2026-09-08 14:26:24 +08:00
parent f2884eefc2
commit 902d6f824f
8 changed files with 641 additions and 22 deletions

View File

@ -387,6 +387,44 @@ bricks.AgentIO = class extends bricks.VBox {
chunk_ended(){
console.log('chunk end');
}
// 切换产线2026-09-07 移动端):重设 chat/menus/model 地址的 pipeline_id 与
// session_idm_<pid>,每产线独立会话不混历史)并清空会话区。
// 不重建控件 —— inputw 的未发草稿/附件保留。
// 入参兼容:字符串 pid / 数据对象 {id|pipeline_id} / bind 事件对象
// method handler 收到的是 event数据在 event.params —— ChipBar select
set_pipeline(p){
if (p && p.params) p = p.params;
var pid = (typeof p === 'string') ? p
: ((p && (p.pipeline_id || p.id)) || '');
if (!pid) return;
function _u(base, pl){
if (!base) return base;
var u = base;
function _set(key, val){
if (new RegExp('[?&]' + key + '=').test(u)){
u = u.replace(new RegExp('([?&]' + key + '=)[^&]*'), '$1' + encodeURIComponent(val));
} else {
u += (u.indexOf('?') >= 0 ? '&' : '?') + key + '=' + encodeURIComponent(val);
}
}
_set('pipeline_id', pl);
_set('session_id', 'm_' + pl);
return u;
}
this._base_chat_url = this._base_chat_url || this.opts.url;
this._base_model_url = this._base_model_url || this.opts.model_dataurl;
this.opts.url = _u(this._base_chat_url, pid);
if (this._base_model_url) {
this.opts.model_dataurl = _u(this._base_model_url, pid);
// 模型下拉按产线过滤capabilities 等参数保留在 _base_model_url 里)
if (this.inputw && this.inputw.set_model_dataurl) {
this.inputw.set_model_dataurl(this.opts.model_dataurl,
{pipeline_id: pid, session_id: 'm_' + pid});
}
}
this.pipeline_id = pid;
this.msg_box.clear_widgets();
}
async user_inputed(e){
this.show_input(e.params);
var params = e.params;

View File

@ -122,6 +122,32 @@ bricks.AgentInput = class extends bricks.VBox {
this.inputfilew.show();
}
// 重设模型下拉数据源2026-09-07 移动端切产线用):销毁重建 UiCode
// 保持其在按钮行中的位置与样式不变。
set_model_dataurl(url, params) {
if (!url) return;
var _mp = bricks.extend({}, params || this.model_selector.opts.params || {});
var old = this.model_selector;
this.model_selector = new bricks.UiCode({
name: 'model_id',
valueField: 'model_id',
textField: 'model_id_text',
dataurl: url,
params: _mp,
cwidth: old.opts.cwidth || 12,
cfontsize: 0.85
});
// 原位替换:按钮行布局中 old 的位置换成新控件
var row = old.parent;
if (row && row.dom_element && old.dom_element) {
row.dom_element.replaceChild(this.model_selector.dom_element, old.dom_element);
var idx = row.children.indexOf(old);
if (idx >= 0) row.children[idx] = this.model_selector;
this.model_selector.parent = row;
old.parent = null;
}
}
getValue() {
return {
prompt: (this.textw.getValue() || {}).prompt || '',

View File

@ -13,7 +13,7 @@ SOURCES=" page_data_loader.js factory.js uitypesdef.js utils.js uitype.js \
line.js pie.js bar.js gobang.js period.js iconbarpage.js \
keypress.js asr.js webspeech.js countdown.js progressbar.js \
qaframe.js svg.js videoplayer.js scatter.js radar.js kline.js \
heatmap.js map.js qr.js textfiles.js agent_input.js agent.js api_doc.js flipcard.js carousel.js draggable.js resourcebrowser.js inherit.js engine.js scene3d.js code_editor.js "
heatmap.js map.js qr.js textfiles.js agent_input.js agent.js api_doc.js flipcard.js carousel.js draggable.js resourcebrowser.js inherit.js engine.js scene3d.js code_editor.js mobile.js "
echo ${SOURCES}
cat ${SOURCES} > ../dist/bricks.js
# uglifyjs --compress --mangle -- ../dist/bricks.js > ../dist/bricks.min.js

View File

@ -992,3 +992,156 @@ hr {
background: var(--sage-bg-dark, #1e1e2e);
color: var(--sage-text-dark, #e0e0e0);
}
/* ══════════ 移动端适配2026-09-07mobile.js 配套样式) ══════════ */
/* 底部 TabBarTabPanel mobile_bar 模式) */
.tabbar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 9000;
display: flex;
align-items: stretch;
background: var(--sage-bg, #ffffff);
border-top: 1px solid #e2e8f0;
padding-bottom: env(safe-area-inset-bottom, 0px);
box-shadow: 0 -2px 12px rgba(15, 23, 42, 0.06);
}
[data-theme="dark"] .tabbar {
background: #0f172a;
border-top-color: #334155;
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.4);
}
.tabbar-item {
position: relative;
min-height: 52px; /* 触控目标 >= 44px */
padding: 5px 0 4px;
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
color: #64748b;
transition: color 0.15s ease;
}
.tabbar-item-active { color: #2563eb; }
.tabbar-item-press { background: rgba(37, 99, 235, 0.06); }
[data-theme="dark"] .tabbar-item { color: #94a3b8; }
[data-theme="dark"] .tabbar-item-active { color: #60a5fa; }
.tabbar-icon {
font-size: 20px;
line-height: 1.15;
text-align: center;
width: 100%;
}
.tabbar-item-active .tabbar-icon { transform: scale(1.06); }
.tabbar-label {
font-size: 11px;
line-height: 1.2;
text-align: center;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tabbar-item-active .tabbar-label { font-weight: 600; }
.tabbar-badge {
position: absolute;
top: 2px;
left: 58%;
min-width: 16px;
height: 16px;
padding: 0 4px;
border-radius: 8px;
background: #ef4444;
color: #fff !important;
font-size: 10px;
font-weight: 700;
line-height: 16px;
text-align: center;
box-sizing: border-box;
}
/* 手机视口下 TabPanel 内容区为底部 TabBar 让位 */
.tabpanel-mobilebar {
padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px)) !important;
box-sizing: border-box;
}
/* 胶囊切换条ChipBar */
.chipbar {
gap: 8px;
padding: 8px 12px;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
align-items: center;
}
.chipbar::-webkit-scrollbar { display: none; }
.chip {
flex: 0 0 auto;
padding: 7px 16px !important;
border-radius: 999px !important;
border: 1px solid #cbd5e1;
background: #ffffff;
color: #334155 !important;
font-size: 13px !important;
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
white-space: nowrap;
transition: all 0.15s ease;
}
.chip-active {
background: #2563eb !important;
border-color: #2563eb;
color: #ffffff !important;
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.35);
}
[data-theme="dark"] .chip {
background: #1e293b;
border-color: #475569;
color: #e2e8f0 !important;
}
[data-theme="dark"] .chip-active {
background: #2563eb !important;
border-color: #2563eb;
color: #ffffff !important;
}
/* ══════════ 手机视口全局规则(<=768px平板按桌面处理 ══════════ */
@media (max-width: 768px) {
/* PopupWindow 全屏化bricks 弹窗行内 width/height/left/top !important 覆盖
* 只针对 .popupwindow 普通 .popup下拉菜单/小提示保持锚定小窗 */
.popupwindow {
width: 100vw !important;
max-width: 100vw !important;
height: 100vh !important;
max-height: 100vh !important;
left: 0 !important;
top: 0 !important;
border-radius: 0 !important;
box-sizing: border-box;
}
/* 输入控件 16pxiOS Safari 对 <16px 输入框聚焦时自动放大页面 */
.inputbox, .auto-textarea, textarea,
input[type="text"], input[type="password"], input[type="number"], select {
font-size: 16px !important;
}
/* 表格横向滚动不挤压 */
.tabular {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* 弹窗/工具栏按钮触控目标 */
.toolbar-button, .popup button, .message button {
min-height: 40px;
}
}
.chip-empty {
color: #94a3b8 !important;
font-size: 13px !important;
padding: 8px 12px !important;
white-space: normal !important;
line-height: 1.5;
}

262
bricks/mobile.js Normal file
View File

@ -0,0 +1,262 @@
// bricks/mobile.js — 移动端适配核心2026-09-07
// 原则:移动适配逻辑全部收在 bricks 框架内,应用层(.ui/.dspy只做声明。
// 提供:
// bricks.is_mobile() — 当前是否手机视口(<=768px平板按桌面
// bricks.on_mobile_mode(fn) — 视口模式变化监听fn(is_mobile)
// bricks.TabBar — 底部标签栏控件手机专用TabPanel mobile_bar 模式使用)
// bricks.ChipBar — 横向胶囊切换条dataurl 动态数据select 事件带整条数据)
var bricks = window.bricks || {};
/* is_mobile utils.js UA+ iPad
* 触摸笔记本判为手机新口径2026-09-07 用户定夺
* 手机 = 手机 UAiPhone/Android 手机/WinPhone横屏也保持手机布局
* 视口宽 <=768px桌面窗口缩窄 响应式进手机布局
* 平板iPad/Android 平板宽度 >768= 桌面布局
* 旧实现已被 popup.js/jsoncall.js/audio.js 等使用覆盖后这些调用点自动
* 获得新口径popup 全屏化只发生在真手机/窄视口平板恢复 70% 弹窗
* 另外 jsoncall 每个请求自动带 _is_mobile=0/1 参数dspy 服务端可感知端类型
*/
(function(){
var mq = window.matchMedia ? window.matchMedia('(max-width: 768px)') : null;
var is_phone_ua = /iPhone|iPod|Windows Phone/i.test(navigator.userAgent)
|| (/Android/i.test(navigator.userAgent) && /Mobile/i.test(navigator.userAgent));
var listeners = [];
bricks.is_mobile = function(){
if (is_phone_ua) return true;
return mq ? mq.matches : false;
};
bricks.on_mobile_mode = function(fn){
if (typeof fn !== 'function') return;
listeners.push(fn);
};
function _notify(){
var m = bricks.is_mobile();
for (var i=0;i<listeners.length;i++){
try { listeners[i](m); } catch(e){ console.log('on_mobile_mode listener error', e); }
}
// 声明式端适配控件JsWidget 注册的 mobile_hidden/desktop_hidden/mobile_opts
var ws = bricks._mobile_adapt_widgets || [];
for (var j=ws.length-1;j>=0;j--){
var w = ws[j];
// 控件销毁后 dom_element 已脱离文档,顺手清理注册表防泄漏
if (!w || !w.dom_element || !w.dom_element.isConnected){
ws.splice(j, 1);
continue;
}
try { w._apply_visibility_mobile(); } catch(e){ console.log('mobile adapt error', e); }
}
// 通知全体控件(文档级事件,供非 bricks 场景使用)
try {
document.documentElement.setAttribute('data-mobile', m ? '1' : '0');
} catch(e){}
}
if (mq){
if (mq.addEventListener) mq.addEventListener('change', _notify);
else if (mq.addListener) mq.addListener(_notify); // 旧 Safari
}
// 初始标记
setTimeout(function(){
document.documentElement.setAttribute('data-mobile', bricks.is_mobile() ? '1' : '0');
}, 0);
// 浏览器 tab 级会话 id原 pipeline-app index_tab.js2026-09-07 收编入框架)。
// sessionStorage 隔离:每个浏览器 tab 一个 sid用于多 tab 项目/会话上下文隔离。
// agent_input.js 的模型下拉 session_id 注入依赖此函数。
window._sid = function(){
var sid = null;
try { sid = sessionStorage.getItem('bricks_sid'); } catch(e){}
if (!sid){
sid = 's' + Date.now().toString(36) + Math.random().toString(36).slice(2,10);
try { sessionStorage.setItem('bricks_sid', sid); } catch(e){}
}
return sid;
};
})();
/* TabBar
底部固定标签栏手机专用
options: {
items: [{name, label, icon(emoji/文字), badge(数字), badge_url(轮询计数地址)}],
badge_interval: 轮询秒数默认 60
}
badge_url 指向的 dspy 返回 {count: n}count>0 显示红色角标
events: command params = item 描述 Toolbar 一致TabPanel 直接复用
*/
bricks.TabBar = class extends bricks.HBox {
constructor(opts){
opts.width = '100%';
opts.height = 'auto';
super(opts);
this.set_css('tabbar');
this.bar_items = opts.items || [];
this.bar_btns = {};
this.active_name = '';
for (var i=0;i<this.bar_items.length;i++){
this._create_item(this.bar_items[i]);
}
this._start_badge_polling(opts.badge_interval || 60);
}
_create_item(item){
var cell = new bricks.VBox({css:'filler tabbar-item', gap:'1px', alignItems:'center'});
var icon = new bricks.Text({otext: item.icon || '•', i18n: true, css:'tabbar-icon', cfontsize:1.3});
var label = new bricks.Text({otext: item.label || item.name || '', i18n: true, css:'tabbar-label'});
cell.add_widget(icon);
cell.add_widget(label);
var badge = null;
if (item.badge){
badge = new bricks.Text({text: String(item.badge), css:'tabbar-badge'});
cell.add_widget(badge);
}
cell.dom_element.style.cursor = 'pointer';
cell.bind('click', this._clicked.bind(this, item));
// 触控反馈
cell.dom_element.addEventListener('touchstart', function(){ cell.set_css('tabbar-item-press'); }, {passive:true});
cell.dom_element.addEventListener('touchend', function(){ cell.set_css('tabbar-item-press', true); }, {passive:true});
this.add_widget(cell);
this.bar_btns[item.name] = {cell: cell, icon: icon, label: label, badge: badge};
}
_clicked(item){
this.set_active(item.name);
this.dispatch('command', item);
}
set_active(name){
this.active_name = name;
for (var k in this.bar_btns){
var b = this.bar_btns[k];
if (k === name) b.cell.set_css('tabbar-item-active');
else b.cell.set_css('tabbar-item-active', true);
}
}
set_badge(name, n){
var b = this.bar_btns[name];
if (!b) return;
if (n > 0){
if (!b.badge){
b.badge = new bricks.Text({text: String(n), css:'tabbar-badge'});
b.cell.add_widget(b.badge);
} else {
b.badge.set_text(String(n));
b.badge.dom_element.style.display = '';
}
} else if (b.badge){
b.badge.dom_element.style.display = 'none';
}
}
// 角标轮询items 里声明 badge_url 的项定时拉取 {count:n} 刷新角标。
// 用裸 fetch 而非 HttpJson —— 未登录时 401 静默跳过,不触发登录弹窗
// (外壳允许 any 访问,登录前轮询不应打扰用户)。
_start_badge_polling(interval_s){
var todo = [];
for (var i=0;i<this.bar_items.length;i++){
if (this.bar_items[i].badge_url) todo.push(this.bar_items[i]);
}
if (!todo.length) return;
var self = this;
async function poll(){
for (var i=0;i<todo.length;i++){
var it = todo[i];
try {
var r = await fetch(it.badge_url, {method:'GET'});
if (!r.ok) continue;
var d = await r.json();
if (d && typeof d.count !== 'undefined'){
self.set_badge(it.name, parseInt(d.count, 10) || 0);
}
} catch(e){}
}
}
poll();
this._badge_timer = setInterval(poll, interval_s * 1000);
}
}
bricks.Factory.register('TabBar', bricks.TabBar);
/* ChipBar
横向滚动胶囊切换条数据来自 dataurldspy 返回 {items:[...], selected:...}
或直接 [...]选中派发 select 事件params = 整条 item 数据
可直接被 bind urlwidget params 合并 pipeline_id 透传给目标 dspy
options: {
dataurl: 数据地址
params: 请求参数
valueField: 值字段名默认 'id'
textField: 显示字段名默认 'name'
selected: 初始选中值
empty_text: 无数据提示默认 '暂无数据'
select_on_load: false 可禁用初始 select 派发默认派发
供联动控件如 AgentIO.set_pipeline 初始化
}
events: select item 数据
*/
bricks.ChipBar = class extends bricks.HScrollPanel {
constructor(opts){
opts.css = (opts.css || '') + ' chipbar';
super(opts);
// HScrollPanel 构造强制 height:'100%'胶囊条是内容高度auto恢复之
this.set_style('height', 'auto');
this.value_field = opts.valueField || 'id';
this.text_field = opts.textField || 'name';
this.chips = {};
this.selected_value = opts.selected || '';
if (this.opts.dataurl){
this.load_data(this.opts.dataurl, this.opts.params || {});
} else if (opts.items){
this.apply_data_items(opts.items, opts.selected || '');
}
}
async load_data(url, params){
var jc = new bricks.HttpJson();
var d = await jc.get(url, {params: params});
if (!d) return;
var items = d.items || d;
if (!Array.isArray(items)) return;
this.apply_data_items(items, d.selected || this.selected_value);
}
apply_data_items(items, selected){
this.data_items = items;
if (!items.length){
var t = new bricks.Text({otext: this.opts.empty_text || '暂无数据', i18n: true, css:'chip-empty'});
this.add_widget(t);
return;
}
for (var i=0;i<items.length;i++){
this._create_chip(items[i]);
}
if (!selected && items.length) selected = items[0][this.value_field];
this.set_selected(selected);
// 初始 select 派发联动控件AgentIO 等)据此完成初始化。
if (this.opts.select_on_load !== false){
for (var j=0;j<items.length;j++){
if (items[j][this.value_field] == selected){
this.dispatch('select', items[j]);
break;
}
}
}
}
_create_chip(item){
var v = item[this.value_field];
var txt = item[this.text_field] || v;
var chip = new bricks.Text({otext: txt, i18n: true, css:'chip'});
chip.dom_element.style.cursor = 'pointer';
chip.bind('click', this._chip_clicked.bind(this, item));
this.add_widget(chip);
this.chips[String(v)] = chip;
}
_chip_clicked(item){
this.set_selected(item[this.value_field]);
this.dispatch('select', item);
}
set_selected(v){
this.selected_value = v;
for (var k in this.chips){
if (k === String(v)) this.chips[k].set_css('chip-active');
else this.chips[k].set_css('chip-active', true);
}
}
}
bricks.Factory.register('ChipBar', bricks.ChipBar);

View File

@ -480,6 +480,7 @@ bricks.PopupWindow = class extends bricks.Popup {
opts.auto_dismiss = false;
opts.auto_destroy = false;
super(opts);
this.set_css('popupwindow'); // 移动端全屏化 CSS 依赖此类(区别于下拉/提示等普通 Popup
this.auto_open = ao;
this.title_bar = new bricks.HBox({css:'titlebar', cheight:2, width:'100%'});
this.moving_w = this.title_bar;

View File

@ -61,9 +61,79 @@ bricks.TabPanel = class extends bricks.Layout {
this.createToolbar();
this.set_css('tabpanel');
this.content_container.set_css('tabpanel-content');
// 移动端底部标签栏模式2026-09-07options.mobile_bar = {items:[{name,label,micon,url}]}
// 手机视口隐藏顶部 tab 条,改用固定底部 TabBar点击 → open_tab框架原生动态 tab
// 桌面视口 TabBar 隐藏,行为与原来完全一致(侧边栏菜单驱动 open_tab
if (this.opts.mobile_bar){
this.createMobileBar();
}
this.show_first_tab();
}
createMobileBar(){
var mb = this.opts.mobile_bar;
var items = ((mb && mb.items) || []).map(function(it){
return {name: it.name, label: it.label || it.name, icon: it.micon || '',
badge_url: it.badge_url || ''};
});
this.mobile_bar = new bricks.TabBar({items: items, badge_interval: mb.badge_interval || 60});
this._mobile_bar_descs = (mb && mb.items) || [];
this.mobile_bar.bind('command', this._mobile_bar_clicked.bind(this));
bricks.Body.add_widget(this.mobile_bar);
var self = this;
this._mobile_bar_apply = function(m){
var show = m && !!self.mobile_bar;
self.mobile_bar.dom_element.style.display = show ? 'flex' : 'none';
self.tab_container.dom_element.style.display = show ? 'none' : '';
if (show) self.set_css('tabpanel-mobilebar');
else self.set_css('tabpanel-mobilebar', true);
if (show){
// 手机首屏:当前 tab 不在底部栏(如桌面默认的 home/商店页)时
// 自动切到第一项产线工作界面1 次都不用点)
var known = false;
for (var i=0;i<self._mobile_bar_descs.length;i++){
if (self._mobile_bar_descs[i].name === self.cur_tab_name){ known = true; break; }
}
if (!known && self._mobile_bar_descs.length){
self._mobile_bar_clicked({name: self._mobile_bar_descs[0].name});
} else {
self.mobile_bar.set_active(self.cur_tab_name);
}
}
};
bricks.on_mobile_mode(this._mobile_bar_apply);
this._mobile_bar_apply(bricks.is_mobile());
}
_mobile_bar_clicked(item){
var desc = null;
for (var i=0;i<this._mobile_bar_descs.length;i++){
if (this._mobile_bar_descs[i].name === item.name){ desc = this._mobile_bar_descs[i]; break; }
}
if (!desc) return;
// refresh:false —— 手机底部 Tab 切换保留各页状态(会话/滚动位置),
// 符合手机操作习惯(微信式);桌面侧边栏 open_tab 的"点击即刷新"语义不受影响
this.open_tab({name: desc.name, label: desc.label, url: desc.url,
removable: false, refresh: false});
this.mobile_bar.set_active(desc.name);
}
show_first_tab(){
// 手机视口 + 配了 mobile_bar首屏由底部栏第一项决定如「产线」工作界面
// 不能再 dispatch items[0](桌面默认 tab如商店页—— 否则后者异步构建完成
// 会把手机首屏覆盖掉。
// 注意去重constructor 里 createMobileBar→_mobile_bar_apply 已同步执行过
// _mobile_bar_clickedcur_tab_name 已置为第一项),此处只需兜底(如
// _mobile_bar_apply 未触发点击的场景),避免 open_tab 二次刷新重复拉取。
if (this.opts.mobile_bar && bricks.is_mobile()){
var descs = this._mobile_bar_descs || [];
var cur_known = false;
for (var i=0;i<descs.length;i++){
if (descs[i].name === this.cur_tab_name){ cur_known = true; break; }
}
if (cur_known) return;
if (descs.length){
this._mobile_bar_clicked({name: descs[0].name});
return;
}
}
this.toolbar.dispatch('command', this.opts.items[0]);
}
createToolbar(){
@ -92,30 +162,33 @@ bricks.TabPanel = class extends bricks.Layout {
}
for (var i=0;i<items.length;i++){
if (tdesc.name == items[i].name){
var w = objget(this.content_buffer, tdesc.name);
if (w && ! items[i].refresh){
this.cur_tab_name = name;
this.switch_content(w);
return;
var w = this.content_buffer[tdesc.name];
// refresh:true 的 tab 每次切换都重建(原语义保留)
if (w && items[i].refresh){
delete this.content_buffer[tdesc.name];
w = null;
}
try {
if (items[i].content instanceof bricks.JsWidget){
w = items[i].content;
} else {
w = await bricks.widgetBuild(items[i].content, this, {});
if (!w){
var c = items[i].content || {"widgettype":"urlwidget","options":{"url":items[i].url}};
try {
if (c instanceof bricks.JsWidget){
w = c;
} else {
w = await bricks.widgetBuild(c, this, {});
}
}
catch (e) {
console.log('except ', e)
}
if (! w){
bricks.debug('TabPanel():create content error', items[i].content);
bricks.debug('TabPanel():create content error', c);
return;
}
this.content_buffer[tdesc.name] = w;
this.cur_tab_name = tdesc.name;
this.switch_content(w);
return;
}
catch (e) {
console.log('except ', e)
}
this.cur_tab_name = tdesc.name;
this.switch_content(w);
return;
}
}
bricks.debug('TabPanel(): click event handled but noting to do', tdesc)
@ -126,11 +199,54 @@ bricks.TabPanel = class extends bricks.Layout {
this.dispatch('switch', w);
w.dispatch('active');
}
add_tab(desc){
var item = this.toolbar.createTool(desc);
if (desc.removable){
this.add_removeable(item);
// 动态 tab 支持(原 pipeline-app index_tab.js 补丁2026-09-07 收编入框架):
// 异步加载 tab content —— 不阻塞 open_tab401 触发登录框,登录后重试。
async _load_tab_content(desc){
if (this.content_buffer[desc.name]){
if (this.cur_tab_name === desc.name) this.switch_content(this.content_buffer[desc.name]);
return;
}
var c = desc.content || {"widgettype":"urlwidget","options":{"url":desc.url}};
var w = await bricks.widgetBuild(c, this, {});
this.content_buffer[desc.name] = w;
if (w && this.cur_tab_name === desc.name) this.switch_content(w);
}
// 动态打开 tab已存在则切换默认刷新内容desc.refresh===false 用缓存),
// 不存在则新增。同步返回:立即切换+高亮content 异步加载。
open_tab(desc){
var existing = null;
for (var i=0;i<this.opts.items.length;i++){
if (this.opts.items[i].name === desc.name){ existing = this.opts.items[i]; break; }
}
if (existing){
var need_refresh = desc.refresh !== false;
if (desc.name !== this.cur_tab_name){
this.cur_tab_name = desc.name;
if (this.toolbar && this.toolbar.click) this.toolbar.click(desc.name);
}
if (need_refresh){
delete this.content_buffer[desc.name];
this._load_tab_content(existing);
} else {
var w = this.content_buffer[desc.name];
if (w) this.switch_content(w);
else this._load_tab_content(existing);
}
return;
}
// 新增
desc.content = desc.content || {"widgettype":"urlwidget","options":{"url":desc.url}};
this.opts.items.push(desc);
this.add_tab(desc);
this.cur_tab_name = desc.name;
if (this.toolbar && this.toolbar.click) this.toolbar.click(desc.name);
this._load_tab_content(desc);
}
add_tab(desc){
// createTool 内部已按 desc.removable 调 Toolbar.add_removable 挂删除按钮;
// TabPanel 自身无 add_removeable 方法(旧实现调它会 TypeError
// 2026-09-07 收编 index_tab.js 修复时一并纠正)
this.toolbar.createTool(desc);
}
tab_removed(event){
var desc = event.params;

View File

@ -41,8 +41,31 @@ bricks.JsWidget = class {
if (this.bgimage){
this.set_bg_image(this.bgimage);
}
// 声明式端适配2026-09-07
// mobile_hidden: true — 手机视口隐藏(桌面显示)
// desktop_hidden: true — 桌面视口隐藏(手机显示)
// mobile_opts: {margin:'0 8px 8px', padding:'4px', ...} — 手机视口下
// 覆盖对应行内样式键,桌面恢复 options 原值(无则清空行内)
// 由 bricks/mobile.js 的 on_mobile_mode 统一驱动,控件自身无脚本。
if (options.mobile_hidden || options.desktop_hidden || options.mobile_opts){
this._apply_visibility_mobile();
if (!bricks._mobile_adapt_widgets) bricks._mobile_adapt_widgets = [];
bricks._mobile_adapt_widgets.push(this);
}
bricks.resize_observer.observe(this.dom_element);
}
_apply_visibility_mobile(){
var m = bricks.is_mobile ? bricks.is_mobile() : false;
var hide = (m && this.opts.mobile_hidden) || (!m && this.opts.desktop_hidden);
this.dom_element.style.display = hide ? 'none' : '';
var mo = this.opts.mobile_opts;
if (mo){
for (var k in mo){
var v = m ? mo[k] : (this.opts[k] !== undefined ? this.opts[k] : '');
this.set_style(k, v);
}
}
}
set_bg_image(url){
var d = this.dom_element;
d.style.backgroundImage = "url('" +url + "')";