From 902d6f824f4814e8d742ffc7c98ff54820fc9b3d Mon Sep 17 00:00:00 2001 From: yumoqing Date: Tue, 8 Sep 2026 14:26:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(mobile):=20=E6=89=8B=E6=9C=BA=E7=AB=AFH5?= =?UTF-8?q?=E9=80=82=E9=85=8D=E6=94=B6=E7=BC=96=E5=85=A5=E6=A1=86=E6=9E=B6?= =?UTF-8?q?=E2=80=94=E2=80=94TabBar/ChipBar=E6=8E=A7=E4=BB=B6+=E5=A3=B0?= =?UTF-8?q?=E6=98=8E=E5=BC=8Fmobile=5Fhidden/mobile=5Fopts+TabPanel=20mobi?= =?UTF-8?q?le=5Fbar=E6=A8=A1=E5=BC=8F+AgentIO.set=5Fpipeline=E5=88=87?= =?UTF-8?q?=E4=BA=A7=E7=BA=BF+index=5Ftab.js=E5=8A=A8=E6=80=81tab=E6=94=B6?= =?UTF-8?q?=E7=BC=96(=E4=BF=AEshow=5Ftabcontent=E6=9C=AA=E5=AE=9A=E4=B9=89?= =?UTF-8?q?name/refresh=E8=AF=AD=E4=B9=89/add=5Ftab=E4=B8=8D=E5=AD=98?= =?UTF-8?q?=E5=9C=A8=E7=9A=84add=5Fremoveable=E4=B8=89=E5=A4=84=E6=97=A7bu?= =?UTF-8?q?g)+PopupWindow=E5=85=A8=E5=B1=8F=E5=8C=96+iOS=2016px=E8=BE=93?= =?UTF-8?q?=E5=85=A5=E9=98=B2=E7=BC=A9=E6=94=BE(2026-09-07=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E9=9C=80=E6=B1=82:=E9=80=82=E9=85=8D=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E4=B8=8D=E5=87=BAbricks=E6=A1=86=E6=9E=B6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bricks/agent.js | 38 ++++++ bricks/agent_input.js | 26 +++++ bricks/build.sh | 2 +- bricks/css/bricks.css | 153 ++++++++++++++++++++++++ bricks/mobile.js | 262 ++++++++++++++++++++++++++++++++++++++++++ bricks/popup.js | 1 + bricks/tab.js | 158 +++++++++++++++++++++---- bricks/widget.js | 23 ++++ 8 files changed, 641 insertions(+), 22 deletions(-) create mode 100644 bricks/mobile.js diff --git a/bricks/agent.js b/bricks/agent.js index cbe4b39..109da68 100644 --- a/bricks/agent.js +++ b/bricks/agent.js @@ -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_,每产线独立会话不混历史)并清空会话区。 + // 不重建控件 —— 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; diff --git a/bricks/agent_input.js b/bricks/agent_input.js index 56a5d8b..4d85c9e 100644 --- a/bricks/agent_input.js +++ b/bricks/agent_input.js @@ -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 || '', diff --git a/bricks/build.sh b/bricks/build.sh index 51b787f..1f8340e 100755 --- a/bricks/build.sh +++ b/bricks/build.sh @@ -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 diff --git a/bricks/css/bricks.css b/bricks/css/bricks.css index efd6e7a..e3ee310 100755 --- a/bricks/css/bricks.css +++ b/bricks/css/bricks.css @@ -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; +} diff --git a/bricks/mobile.js b/bricks/mobile.js new file mode 100644 index 0000000..4a3c9a4 --- /dev/null +++ b/bricks/mobile.js @@ -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=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 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