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:
parent
f2884eefc2
commit
902d6f824f
@ -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_id(m_<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;
|
||||
|
||||
@ -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 || '',
|
||||
|
||||
@ -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
|
||||
|
||||
@ -992,3 +992,156 @@ hr {
|
||||
background: var(--sage-bg-dark, #1e1e2e);
|
||||
color: var(--sage-text-dark, #e0e0e0);
|
||||
}
|
||||
|
||||
/* ══════════ 移动端适配(2026-09-07,mobile.js 配套样式) ══════════ */
|
||||
|
||||
/* 底部 TabBar(TabPanel 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;
|
||||
}
|
||||
/* 输入控件 16px:iOS 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
262
bricks/mobile.js
Normal 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 用户定夺):
|
||||
* 手机 = 手机 UA(iPhone/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.js,2026-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 ══════════════════════════
|
||||
横向滚动胶囊切换条。数据来自 dataurl(dspy 返回 {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);
|
||||
@ -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;
|
||||
|
||||
158
bricks/tab.js
158
bricks/tab.js
@ -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-07):options.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_clicked(cur_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_tab;401 触发登录框,登录后重试。
|
||||
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;
|
||||
|
||||
@ -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 + "')";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user