--- name: bricks-dev-patterns description: Bricks Menu/target/DOM/语言/主题/DSPY陷阱。改Bricks前必读。 tags: [bricks, menu, language, theme, dspy, pitfall] --- # Bricks 开发高级模式 开发 Bricks/Sage 应用时反复踩坑的非显而易见模式。 --- ## 1. Menu 控件 ```json {"widgettype": "Menu", "options": { "menuitem_css": "menuitem", "items": [ {"name": "id", "label": "文字", "url": "{{entire_url('/path')}}", "target": "app.主内容id"}, {"name": "group", "label": "分组", "items": [ // 嵌套用 items, 不用 children {"name": "sub", "label": "子项", "url": "...", "target": "app.主内容id"} ]} ] }} ``` - `target` 必须 `"app.xxx"`(DOM closest/querySelector 不能跨兄弟节点) - `icon` 不用 emoji(bricks 当作 URL → 401),设 `""` - 主内容 id 放 VScrollPanel,不放 HBox 父容器 --- ## 2. 语言切换 `/i18n/menu.ui`(需 `any` 权限): ```json {"widgettype": "Menu", "options": {"cwidth": 11, "items": [ {"name": "zh", "label": "中文", "script": "bricks.app.change_language('zh')"}, {"name": "en", "label": "English", "script": "bricks.app.change_language('en')"} ]}} ``` 方法在 `bricks.app.change_language(lang)`。 --- ## 3. 主题切换 ```javascript var h = document.documentElement; var t = h.getAttribute('data-theme') || 'light'; var n = t == 'light' ? 'dark' : 'light'; h.setAttribute('data-theme', n); var b = bricks.getWidgetById('theme_toggle_btn', bricks.app); if (b) { b.opts.label = n == 'light' ? '🌙' : '☀️'; b.dom_element.textContent = b.opts.label; } ``` Button 无 `refresh()`,用 `dom_element.textContent`。 --- ## 4. DSPY 陷阱 - **f-string** → 用字符串拼接:`'算力池: ' + str(count)` - **add/update datetime**:空串插入 → 500,加清理: ```python for k in list(ns.keys()): if k.endswith('_at') or k.endswith('_time') or k == 'last_heartbeat': if ns.get(k, '') in ('', None, 'None'): ns[k] = None ``` - **CRUD stub**:`return {'status':'ok'}` 无 `sor.C/U/D()` → 桩代码 --- ## 5. CRUD 工具栏 自动生成 JSON 中 `"tools": []` → **删除**这行,框架自动生成按钮。 --- ## 6. 概览 Stats 返回 HBox + VBox 卡片(不用 Text widget): ```python return {'widgettype': 'HBox', 'options': {'gap': '16px'}, 'subwidgets': [ {'widgettype': 'VBox', 'options': {'bgcolor': '#eff6ff', 'padding': '16px', 'width': '25%', 'border': '1px solid #dbeafe', 'borderRadius': '8px'}, 'subwidgets': [ {'widgettype': 'Text', 'options': {'otext': '标签', 'cfontsize': 0.7}}, {'widgettype': 'Text', 'options': {'otext': '数值', 'cfontsize': 2, 'fontWeight': 'bold'}} ]} ]} ``` --- ## 7. 权限隔离 | 角色 | 范围 | |------|------| | `any` | stats API, index.ui, menu.ui, overview.ui, 静态资源 | | `logined` | get_*.dspy, CRUD create/update/delete | **反模式**:CRUD 加 `any` → 匿名可增删改。 --- ## 8. Button bind → script 反馈 按钮用 `binds` 触发 script 时: - 反馈弹窗用官方封装 `bricks.show_message({title, message})` / `bricks.show_error({title, message})`(自带默认尺寸)。`new bricks.Message({...})` 构造时 `auto_open=true` 已自动打开,`.open()` 冗余。 - `bricks.universal_handler` 是 **async**:点击后**立刻**检查 fetch 调用/弹窗会得假阴性(fetch 还没执行)。验证脚本要 `await new Promise(r => setTimeout(r, 800))` 再断言,否则会误判"按钮 script 没生效"。 - 快速诊断 bind 是否真的绑定:`btn.dispatchEvent(new Event('click'))` 会直接触发 bind handler(不经过 label 的 target_clicked 链路)。 --- ## 9. .ui 文件直接访问 浏览器直接访问 `/module/xxx/index.ui` 返回**原始 JSON**,不是渲染页。.ui 必须经 bricks 框架加载(从 Menu/urlwidget 点击进入)。别用 browser_navigate 直连 .ui URL 去判断渲染结果。 --- ## 10. Menu 子菜单:items vs submenu vs 裸数组 - 内联子菜单用 `items: [{name,label,url}, ...]`(见 §1)。 - `submenu` 字段指向的是**完整 Menu widget 的 .ui**(含 `"widgettype": "Menu"`)。 - 裸数组(如 `rbac/admin_menu.ui` 的 `[{name,label,url},...]`)**不是** Menu widget,不能被 `submenu` 直接加载——要么把数组内联进父菜单 `items`,要么包一层 Menu widget。 --- ## 11. 编辑 .ui/.json 的 patch 陷阱 改 Menu/JSON 前先 **re-read 文件**——部署过程里文件可能被外部改过。patch 的模糊匹配在文件已变化时会错位:曾把 `app_audit` 菜单项误删、并把 `tenant` 复制两份。改后必验证:列出所有 `name`,检查重复项(`[n for n in names if names.count(n)>1]`)。 --- ## 12. Text 控件默认 halign 是 'center' `widget.js` 里 `options.halign = options.halign || 'center'`——**Text 不写 `halign` 时文字默认居中,不是左对齐**。 - 文件列表、表格行、标签等任何要左对齐的文本必须显式 `"halign": "left"`。 - 尤其 Text 同时加 `"css": "filler"`(flex-grow 撑满剩余空间)时,文字会在撑开的盒子里居中,视觉上"不靠图标"。文件名行 = 图标 + 名称(`halign:"left"` + `css:"filler"`) + 大小,名称必须显式 halign left。 --- ## 13. 前端改动"没生效" → 用 ?v= 缓存破除,别只让用户 Ctrl+F5 `bricks.js`/`bricks.css` 响应只有 `ETag`/`Last-Modified`、**没有 `Cache-Control`**,浏览器按启发式缓存(新鲜期 ≈ 距 Last-Modified 的 10%)。用户 Ctrl+F5/清缓存后仍可能拿到旧版,启发式新鲜期不可控。 健壮做法:在 `bricks/header.tmpl` 的 script/link 标签加版本查询串并每次部署递增: ```html ``` 改源文件后同时 bump 版本号 → 浏览器当全新 URL 强制拉取,普通刷新即可。`entire_url` 之外的 `?v=` 会被 ahserver 忽略(按路径 serve,query 不影响)。 **教训**:诊断前端"改了没生效"时,别拿"浏览器缓存"当未经验证的根因下结论——用户已明确清过缓存仍复现,就该怀疑代码本身,改用 console.log 探针 + ?v= 破除缓存再看实际数据。