UNPKG

@nocobase/plugin-block-lowcode

Version:

A block for executing custom JavaScript code and dynamically updating HTML content.

952 lines (797 loc) 36.3 kB
# 低代码区块文档 低代码区块让用户能够用纯 JavaScript 动态创建和运行自定义的交互式前端组件。 --- ## 全局上下文对象 `ctx` 低代码区块为用户代码提供了统一的全局上下文对象 `ctx`。你可以通过解构的方式,快速访问常用变量和方法: ```js type LowcodeContext = { element: HTMLElement; model: FlowModel; i18n: I18next; requirejs: (modules: string[], callback: Function) => void; requireAsync: (modules: string | string[]) => Promise<any>; loadCSS: (url: string) => Promise<void>; getModelById: (uid: string) => FlowModel | null; initResource(typeof APIResource, options: AxiosRequestConfig): APIResource; request: (options: AxiosRequestConfig) => Promise<any>; router: RemixRouter; auth: { role: string; locale: string; token: string; user: any; }; Resources: { APIResource: typeof APIResource; SingleRecordResource: typeof SingleRecordResource; MultiRecordResource: typeof MultiRecordResource; // ...其他扩展资源类型 }; React: typeof React; Components: { antd: typeof import('antd'); // 可扩展更多组件库 }; flowEngine: FlowEngine; flowContext: FlowContext; auth: { role?: string; locale?: string; token?: string; user?: User; }; }; declare const ctx: LowcodeContext; ``` --- ## 核心属性 ### `ctx.element` * **类型**:`HTMLElement` * **说明**:当前组件的根 DOM 元素。每个低代码区块会被渲染到一个独立的 DOM 元素中,`element` 即为该元素的引用。你可以通过它进行内容渲染、事件绑定、集成第三方前端库等操作。该元素的生命周期与区块一致,区块销毁时会自动清理。 * **使用场景**:适用于自定义渲染、挂载第三方库、绑定事件监听等。 * **注意事项**: - 请勿直接替换 `element` 节点本身(如通过 `replaceWith` 或重新赋值),否则会导致区块生命周期异常。 - 推荐只操作 `element` 的内容或子节点,例如通过 `element.innerHTML`、`appendChild`、`addEventListener` 等方式进行 DOM 操作。 - 区块销毁时会自动清理该元素及其事件,无需手动移除。 示例一: ```ts ctx.element.innerHTML = ` <div style="padding: 24px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; max-width: 600px;"> <h2 style="color: #1890ff; margin: 0 0 20px 0; font-size: 24px; font-weight: 600;"> 🚀 Welcome to Lowcode Block </h2> <p style="color: #666; margin-bottom: 24px; font-size: 16px;"> Build interactive components with JavaScript and external libraries </p> <div style="background: #f8f9fa; border-radius: 8px; padding: 20px; margin-bottom: 20px;"> <h3 style="color: #333; margin: 0 0 16px 0; font-size: 18px;">✨ Key Features</h3> <ul style="margin: 0; padding-left: 20px; color: #555;"> <li style="margin-bottom: 8px;">🎨 <strong>Custom JavaScript execution</strong> - Full programming capabilities</li> <li style="margin-bottom: 8px;">📚 <strong>External library support</strong> - Load any npm package or CDN library</li> <li style="margin-bottom: 8px;">🔗 <strong>NocoBase API integration</strong> - Access your data and collections</li> <li style="margin-bottom: 8px;">💡 <strong>Async/await support</strong> - Handle asynchronous operations</li> <li style="margin-bottom: 8px;">🎯 <strong>Direct DOM manipulation</strong> - Full control over rendering</li> </ul> </div> <div style="background: #e6f7ff; border-left: 4px solid #1890ff; padding: 16px; border-radius: 4px;"> <p style="margin: 0; color: #333; font-size: 14px;"> 💡 <strong>Ready to start?</strong> Replace this code with your custom JavaScript to build amazing components! </p> </div> </div> `; ``` 示例二: ```ts ctx.element.style.height = '400px'; const echarts = await ctx.requireAsync('https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js'); const chart = echarts.init(ctx.element); // 生成随机数据 const categories = ['A', 'B', 'C', 'D', 'E', 'F']; const randomData = categories.map(() => Math.floor(Math.random() * 50) + 1); const option = { title: { text: 'ECharts 示例(随机数据)' }, tooltip: {}, xAxis: { data: categories }, yAxis: {}, series: [{ name: '销量', type: 'bar', data: randomData }] }; chart.setOption(option); chart.resize(); window.addEventListener('resize', () => chart.resize()); ctx.model.on('destroy', () => { chart.dispose(); }); ``` ### `ctx.model` * **类型**:[FlowModel](https://pr-7056.client.docs-cn.nocobase.com/core/flow-engine/flow-model) * **说明**:`model` 是当前区块的 ViewModel(视图模型),基于 NocoBase 的 FlowModel 实现。它用于管理区块的数据状态、事件和响应式更新。在低代码区块中,`model` 最常用的功能是调用 `rerender()` 方法以重渲染当前组件,实现视图与数据的联动。你还可以通过 `model.on()` 监听数据或事件变化。 * **使用场景**: - 主动刷新视图(如数据变更后调用 `model.rerender()`) - 监听和响应数据、生命周期事件(如 `model.on('destroy', ...)`) * **注意事项**: - 调用 `rerender()` 后,确保视图渲染逻辑依赖于 `model` 的数据。 - 多区块联动时,注意事件解绑和数据同步,防止内存泄漏。 - 如需复杂数据流转,建议结合 resource 或全局状态管理。 低代码场景里 model 常用的属性和方法 - model.resource - model.rerender() - model.on('destroy', ...) 示例 在区块销毁时手动调用 chart.dispose() 释放 ECharts 实例资源。 ```ts ctx.model.on('destroy', () => { chart.dispose(); }); ``` ### `ctx.i18n` * **类型**:`I18next` * **说明**:国际化对象,基于 [i18next](https://www.i18next.com/) 实现。它提供了多语言文本的管理和切换能力,支持动态加载语言包、参数替换、复数等高级特性。通过 `i18n.t('key')` 获取当前语言下的翻译文本。 * **使用场景**:多语言文本渲染、动态切换语言。 * **注意事项**:使用 i18n.t('key') 获取翻译文本。 示例:中文和英文之间切换,区块的文案会跟着变化。 ```ts const zhCN = { hello: "你好", welcome_user: "欢迎,{{user}}!" }; const enUS = { hello: "Hello", welcome_user: "Welcome, {{user}}!" }; // 添加中文包 ctx.i18n.addResourceBundle('zh-CN', 'ns1', zhCN, true, true); // 添加英文包 ctx.i18n.addResourceBundle('en-US', 'ns1', enUS, true, true); ctx.element.innerHTML = ctx.i18n.t('welcome_user', { user: 'Tom', ns: 'ns1' }); ``` ### `ctx.Resources` * **类型**:`{ APIResource, SingleRecordResource, MultiRecordResource, ... }` * **说明**:包含所有可用的资源类型构造函数。你可以通过 `ctx.Resources` 获取并传递给 `initResource` 的 `use` 字段,灵活创建不同类型的资源实例。常用的有: - `APIResource`:简单的数据请求场景。 - `SingleRecordResource`:单条数据资源,适合详情页、个人信息等场景。 - `MultiRecordResource`:多条数据资源,适合批量操作、复杂数据结构等场景。 * **使用场景**:需要根据业务需求选择不同的数据交互模式时,通过 `ctx.Resources` 获取对应的资源类型。 * **注意事项**: - 推荐始终通过 `ctx.Resources` 获取资源类型,避免直接从全局导入,确保兼容性和可维护性。 - 可扩展:如有自定义资源类型,也可通过插件机制扩展到 `ctx.Resources`。 Resource 相关类的接口说明,更多说明参考 [FlowResource 及资源体系 ](https://pr-7056.client.docs-cn.nocobase.com/core/flow-engine/flow-resource) ```ts // FlowResource interface interface IFlowResource<TData = any> { getData(): TData; setData(value: TData): this; getMeta(metaKey?: string): any; setMeta(meta: Record<string, any>): this; } // APIResource interface interface IAPIResource<TData = any> extends IFlowResource<TData> { loading: boolean; setAPIClient(api: APIClient): this; getURL(): string; setURL(value: string): this; clearRequestParameters(): this; setRequestParameters(params: Record<string, any>): this; setRequestMethod(method: string): this; addRequestHeader(key: string, value: string): this; removeRequestHeader(key: string): this; addRequestParameter(key: string, value: any): this; getRequestParameter(key: string): any | null; removeRequestParameter(key: string): this; setRequestBody(data: any): this; setRequestOptions(key: string, value: any): this; refresh(): Promise<void>; } // BaseRecordResource interface interface IBaseRecordResource<TData = any> extends IAPIResource<TData> { setResourceName(resourceName: string): this; getResourceName(): string; setSourceId(sourceId: string | number): this; getSourceId(): string | number; setDataSourceKey(dataSourceKey: string): this; getDataSourceKey(): string; setFilter(filter: Record<string, any>): this; getFilter(): Record<string, any>; resetFilter(): void; addFilterGroup(key: string, filter: any): void; removeFilterGroup(key: string): void; setAppends(appends: string[]): this; getAppends(): string[]; addAppends(appends: string | string[]): this; removeAppends(appends: string | string[]): this; setFilterByTk(filterByTk: string | number | string[] | number[]): this; getFilterByTk(): string | number | string[] | number[]; setFields(fields: string[] | string): this; getFields(): string[]; setSort(sort: string | string[]): this; getSort(): string[]; setExcept(except: string | string[]): this; getExcept(): string[]; setWhitelist(whitelist: string | string[]): this; getWhitelist(): string[]; setBlacklist(blacklist: string | string[]): this; getBlacklist(): string[]; refresh(): Promise<void>; } // SingleRecordResource interface interface ISingleRecordResource<TData = any> extends IBaseRecordResource<TData> { setFilterByTk(filterByTk: string | number): this; save(data: TData): Promise<void>; destroy(): Promise<void>; refresh(): Promise<void>; } // MultiRecordResource interface interface IMultiRecordResource<TDataItem = any> extends IBaseRecordResource<TDataItem[]> { setSelectedRows(selectedRows: TDataItem[]): this; getSelectedRows(): TDataItem[]; setPage(page: number): this; getPage(): number; setPageSize(pageSize: number): this; getPageSize(): number; getCell(rowIndex: number, columnKey: string): TDataItem | undefined; next(): Promise<void>; previous(): Promise<void>; goto(page: number): Promise<void>; create(data: TDataItem): Promise<void>; update(filterByTk: string | number, data: Partial<TDataItem>): Promise<void>; destroySelectedRows(): Promise<void>; destroy(filterByTk: string | number | string[] | number[] | TDataItem | TDataItem[]): Promise<void>; refresh(): Promise<void>; } ``` * **示例**: 自定义详情区块(使用 SingleRecordResource) ```ts // 初始化资源,仅需调用一次 ctx.initResource(ctx.Resources.SingleRecordResource); const resource = ctx.model.resource; resource.setDataSourceKey('main'); resource.setResourceName('users'); const uid = ctx.model.uid; // 统一使用 model.uid 作为唯一标识 // 表单 HTML 片段 const renderFilterForm = () => ` <form id="userFilterForm_${uid}" style="margin-bottom:16px;"> <input type="text" id="userIdInput_${uid}" placeholder="用户ID" style="margin-right:8px;" /> <button type="submit">筛选</button> </form> <div id="tableContainer_${uid}"></div> `; // 绑定筛选表单事件 function bindFilterFormSubmit() { const form = document.getElementById(`userFilterForm_${uid}`); const input = document.getElementById(`userIdInput_${uid}`); if (!form || !input) return; form.onsubmit = async (e) => { e.preventDefault(); const id = input.value.trim(); const model = ctx.model; if (id && model?.resource?.setFilterByTk) { model.resource.setFilterByTk(id); model.rerender(); } }; } // 渲染用户详情 async function renderUserView() { // 若未指定筛选条件,则提示用户先筛选 if (!resource.getFilterByTk()) { ctx.element.innerHTML = ` ${renderFilterForm()} <div style="padding:24px;color:#999;text-align:center;"> <span>请先输入用户 ID 后查看详情</span> </div> `; bindFilterFormSubmit(); return; } await resource.refresh(); const data = resource.getData(); if (!data) { ctx.element.innerHTML = `${renderFilterForm()}<div style="padding:24px;color:#f00;">未找到用户数据。</div>`; bindFilterFormSubmit(); return; } ctx.element.innerHTML = ` ${renderFilterForm()} <div style="padding:24px;max-width:480px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;"> <h2 style="color:#1890ff;margin-bottom:20px;">用户详情</h2> <table style="width:100%;border-collapse:collapse;"> <tr><td style="font-weight:bold;width:120px;">ID</td><td>${data.id ?? ''}</td></tr> <tr><td style="font-weight:bold;">昵称</td><td>${data.nickname ?? ''}</td></tr> <tr><td style="font-weight:bold;">用户名</td><td>${data.username ?? ''}</td></tr> <tr><td style="font-weight:bold;">邮箱</td><td>${data.email ?? ''}</td></tr> <tr><td style="font-weight:bold;">手机号</td><td>${data.phone ?? ''}</td></tr> <tr><td style="font-weight:bold;">注册时间</td><td>${data.createdAt ? new Date(data.createdAt).toLocaleString() : ''}</td></tr> <tr><td style="font-weight:bold;">最后修改</td><td>${data.updatedAt ? new Date(data.updatedAt).toLocaleString() : ''}</td></tr> </table> </div> `; bindFilterFormSubmit(); } // 初次渲染 await renderUserView(); ``` 自定义的快捷编辑表格区块(使用 MultiRecordResource) ```ts // 初始化资源,只需调用一次 ctx.initResource(ctx.Resources.MultiRecordResource); const resource = ctx.model.resource; resource.setDataSourceKey('main'); resource.setResourceName('users'); resource.setPageSize(10); resource.setSort('-createdAt'); const uid = ctx.model.uid; // 统一用 model.uid 作为唯一标识 async function renderTable({ page }) { resource.setPage(page); await resource.refresh(); const data = resource.getData() || []; const pageSize = resource.getPageSize(); const meta = resource.getMeta() || {}; const total = meta.count || data.length; ctx.element.innerHTML = ` <div style="margin-bottom:16px;"> <form id="addUserForm_${uid}" style="display:inline-block;margin-right:16px;"> <input type="text" id="nicknameInput_${uid}" placeholder="昵称" style="width:100px;margin-right:8px;" /> <input type="text" id="usernameInput_${uid}" placeholder="用户名" style="width:100px;margin-right:8px;" /> <button type="submit">新增</button> </form> </div> <table border="1" cellpadding="6" style="border-collapse:collapse;width:100%;margin-bottom:12px;"> <thead> <tr> <th>ID</th> <th>昵称</th> <th>用户名</th> <th>邮箱</th> <th>注册时间</th> <th>操作</th> </tr> </thead> <tbody> ${data.map(item => ` <tr> <td>${item.id}</td> <td> <input type="text" value="${item.nickname ?? ''}" style="width:80px;" data-id="${item.id}" class="edit-nickname-${uid}" /> </td> <td> <input type="text" value="${item.username ?? ''}" style="width:80px;" data-id="${item.id}" class="edit-username-${uid}" /> </td> <td>${item.email ?? ''}</td> <td>${item.createdAt ? new Date(item.createdAt).toLocaleString() : ''}</td> <td> <button data-id="${item.id}" class="saveBtn_${uid}">保存</button> <button data-id="${item.id}" class="deleteBtn_${uid}" style="color:#f00;">删除</button> </td> </tr> `).join('')} </tbody> </table> <div style="margin-bottom:16px;"> 共 ${total} 条,每页 ${pageSize} 条,当前第 ${page} 页 <button id="prevPage_${uid}" ${page <= 1 ? 'disabled' : ''}>上一页</button> <button id="nextPage_${uid}" ${(page * pageSize >= total) ? 'disabled' : ''}>下一页</button> </div> `; // 分页 document.getElementById(`prevPage_${uid}`).onclick = () => { if (page > 1) { renderTable({ page: page - 1 }); } }; document.getElementById(`nextPage_${uid}`).onclick = () => { if (page * pageSize < total) { renderTable({ page: page + 1 }); } }; // 新增 document.getElementById(`addUserForm_${uid}`).onsubmit = async (e) => { e.preventDefault(); const nickname = document.getElementById(`nicknameInput_${uid}`).value.trim(); const username = document.getElementById(`usernameInput_${uid}`).value.trim(); if (!nickname || !username) { alert('昵称和用户名不能为空'); return; } await resource.create({ nickname, username }); renderTable({ page }); }; // 保存(编辑) Array.from(document.getElementsByClassName(`saveBtn_${uid}`)).forEach(btn => { btn.onclick = async (e) => { const id = btn.getAttribute('data-id'); const nickname = document.querySelector(`.edit-nickname-${uid}[data-id="${id}"]`).value.trim(); const username = document.querySelector(`.edit-username-${uid}[data-id="${id}"]`).value.trim(); await resource.update(id, { nickname, username }); renderTable({ page }); }; }); // 删除 Array.from(document.getElementsByClassName(`deleteBtn_${uid}`)).forEach(btn => { btn.onclick = async (e) => { const id = btn.getAttribute('data-id'); if (confirm('确定要删除该用户吗?')) { await resource.destroy(id); renderTable({ page }); } }; }); } // 首次渲染 await renderTable({ page: 1 }); ``` ### `ctx.router` * **类型**:`RemixRouter` * **说明**:NocoBase 内置的路由对象,基于 React Router 的 RemixRouter 实现。可用于在低代码区块中进行页面跳转、路由导航、获取当前路径等操作,支持 push、replace、goBack 等常用方法。 * **使用场景**:需要在区块中跳转到其他页面、动态导航、响应用户操作时。 * **常用方法**: - `ctx.router.navigate(path, options)`:跳转到指定路径,`options` 支持 `{ replace: true }` 等参数。 - `ctx.router.location`:获取当前路由信息。 * **示例**: ```js const uid = ctx.model.uid; ctx.element.innerHTML = ` <button id="gotoAdminBtn_${uid}" style="padding: 8px 16px; font-size: 16px;"> 跳转到后台管理首页 </button> `; document.getElementById(`gotoAdminBtn_${uid}`).onclick = () => { ctx.router.navigate('/admin/'); }; ``` ### `ctx.auth` * **类型**:`{ role: string, locale: string, token: string, user: any }` * **说明**:当前用户的认证信息上下文,包含用户角色、语言设置、认证令牌和用户详细信息。通过此对象可以获取当前登录用户的基本信息,用于实现基于用户身份的个性化功能或权限控制。 * **使用场景**:需要根据用户身份显示不同内容、实现权限控制、个性化展示等。 * **属性说明**: - `ctx.auth.role`:当前用户的角色信息 - `ctx.auth.locale`:当前用户的语言环境设置 - `ctx.auth.token`:当前用户的认证令牌 - `ctx.auth.user`:当前用户的详细信息对象 * **示例**: ```js ctx.element.innerHTML = ` <div style="padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;"> <h3>用户信息</h3> <p><strong>用户:</strong>${ctx.auth.user?.nickname || ctx.auth.user?.email || '游客'}</p> <p><strong>角色:</strong>${ctx.auth.role || '未设置'}</p> <p><strong>语言:</strong>${ctx.auth.locale || '未设置'}</p> </div> `; ``` --- ### `ctx.React` ```ts const React = ctx.React; const ReactDOM = ctx.ReactDOM; function App() { return React.createElement('h2', null, 'Hello from React in Lowcode!'); } const root = ReactDOM.createRoot(ctx.element); root.render(React.createElement(App)); ``` ### `ctx.Components` 使用 ctx.Components.antd 渲染 Ant Design 按钮和输入框 ```tsx const React = ctx.React; const ReactDOM = ctx.ReactDOM; const { Button, Input } = ctx.Components.antd; function App() { return React.createElement('div', null, React.createElement(Button, { type: 'primary' }, '提交'), React.createElement(Input, { placeholder: '请输入内容', style: { width: 200, marginLeft: 8 } }) ); } const root = ReactDOM.createRoot(ctx.element); root.render(React.createElement(App)); ``` ## 常用方法 ### `ctx.initResource(ResourceClass, options: AxiosRequestConfig): ResourceInstance` * **类型**:`(ResourceClass: typeof APIResource | typeof SingleRecordResource | typeof MultiRecordResource, options?: AxiosRequestConfig) => ResourceInstance` * **说明**:用于初始化并获取当前区块的资源实例。通过传入资源类型构造函数(如 `APIResource`、`SingleRecordResource`、`MultiRecordResource` 等)和可选的请求配置,灵活创建不同类型的资源对象,便于管理和操作数据。该方法只会在当前区块生命周期内执行一次,如果 `ctx.resource` 已存在,则不会重复初始化,后续调用会返回同一个实例。 * **参数**: * `ResourceClass`:资源类型构造函数,支持 `APIResource`、`SingleRecordResource`、`MultiRecordResource` 等。 * `options`:可选,Axios 的请求配置,会通过 `setRequestOptions` 注入到资源实例中。 * **返回**:对应类型的资源实例。 * **使用场景**:需要自定义资源类型、管理多种数据结构或特殊数据交互场景。 * **注意事项**: - 推荐始终通过 `ctx.initResource` 创建和获取资源实例,便于后续扩展和维护。 - 该方法只会初始化一次,区块重渲染也不会重建。 - 一个 Model 组件只有一个 resource 示例,不同的组件,可以通过 model.resource 操作目前资源。 * **示例** 自定义表格区块,基于 MultiRecordResource 实现表格数据展示,带分页,支持筛选 ```ts ctx.initResource(ctx.Resources.MultiRecordResource); const resource = ctx.model.resource; resource.setDataSourceKey('main'); resource.setResourceName('users'); resource.setPageSize(10); resource.setSort('-createdAt'); const uid = ctx.model.uid; // 统一用 model.uid 作为唯一标识 // 渲染筛选表单和表格 async function renderTable({ page = 1, nickname = '' } = {}) { resource.setPage(page); if (nickname) { resource.addFilterGroup(uid, { 'nickname.$includes': nickname }); } else { resource.removeFilterGroup(uid); } await resource.refresh(); const data = resource.getData() || []; const meta = resource.getMeta() || {}; const pageSize = resource.getPageSize(); const total = meta.count || data.length; ctx.element.innerHTML = ` <form id="filterForm_${uid}" style="margin-bottom:16px;"> <input type="text" id="nicknameInput_${uid}" placeholder="昵称" style="margin-right:8px;" value="${nickname}" /> <button type="submit">筛选</button> </form> <table border="1" cellpadding="6" style="border-collapse:collapse;width:100%;margin-bottom:12px;"> <thead> <tr> <th>ID</th> <th>昵称</th> <th>用户名</th> <th>邮箱</th> <th>注册时间</th> </tr> </thead> <tbody> ${data.map(item => ` <tr> <td>${item.id}</td> <td>${item.nickname || ''}</td> <td>${item.username || ''}</td> <td>${item.email || ''}</td> <td>${item.createdAt ? new Date(item.createdAt).toLocaleString() : ''}</td> </tr> `).join('')} </tbody> </table> <div> 共 ${total} 条,每页 ${pageSize} 条,当前第 ${meta.page || page} 页 <button id="prevPage_${uid}" ${meta.page <= 1 ? 'disabled' : ''}>上一页</button> <button id="nextPage_${uid}" ${(meta.page * pageSize >= total) ? 'disabled' : ''}>下一页</button> </div> `; // 筛选事件 document.getElementById(`filterForm_${uid}`).onsubmit = async (e) => { e.preventDefault(); const nickname = document.getElementById(`nicknameInput_${uid}`).value.trim(); // nickname 存于 meta 中,避免重渲染时筛选被清空 resource.setMeta({ nickname }); renderTable({ page: 1, nickname }); }; // 分页事件 document.getElementById(`prevPage_${uid}`).onclick = () => { if (meta.page > 1) { renderTable({ page: meta.page - 1, filter }); } }; document.getElementById(`nextPage_${uid}`).onclick = () => { if (meta.page * pageSize < total) { renderTable({ page: meta.page + 1, filter }); } }; } // 从 meta 获取 nickname,避免重渲染时筛选被清空 await renderTable({ nickname: resource.getMeta('nickname') }); ``` 多来源筛选,新增一个筛选区块,示例的 targetUid 替换为目标表格区块的 model uid。 ```ts const uid = ctx.model.uid; const targetUid = '6ddac206c67'; // 替换为表格区块的 model uid // 多来源筛选表单示例 ctx.element.innerHTML = ` <form id="filterForm_${uid}" style="margin-bottom:16px;"> <input type="text" id="nicknameInput_${uid}" placeholder="昵称" style="margin-right:8px;" /> <input type="text" id="usernameInput_${uid}" placeholder="用户名" style="margin-right:8px;" /> <button type="submit">筛选</button> <button id="resetBtn_${uid}" type="button" style="margin-left:8px;">重置</button> </form> `; // 筛选表单事件 document.getElementById(`filterForm_${uid}`).onsubmit = async (e) => { e.preventDefault(); const nickname = document.getElementById(`nicknameInput_${uid}`).value.trim(); const username = document.getElementById(`usernameInput_${uid}`).value.trim(); const model = ctx.getModelById(targetUid); if (!model) return; // 使用 addFilterGroup 组合多条件筛选 const filterGroup = {}; if (nickname) filterGroup['nickname.$includes'] = nickname; if (username) filterGroup['username.$includes'] = username; if (Object.keys(filterGroup).length) { model.resource.addFilterGroup(uid, filterGroup); } else { model.resource.removeFilterGroup(uid); } model.rerender(); }; // 重置按钮事件 document.getElementById(`resetBtn_${uid}`).onclick = () => { document.getElementById(`nicknameInput_${uid}`).value = ''; document.getElementById(`usernameInput_${uid}`).value = ''; const model = ctx.getModelById(targetUid); if (model) { model.resource.removeFilterGroup(uid); model.rerender(); } }; ``` ### `ctx.requirejs(modules: string[], callback: Function): void` * **说明**:同步加载外部 JavaScript 库,基于 RequireJS 实现。该方法适合需要兼容老代码或同步依赖的场景。加载的模块会被缓存,后续调用会直接返回已加载的模块。回调函数会在所有模块加载完成后执行,参数为各模块的导出对象。 * **参数**: * `modules`:需要加载的模块名数组。 * `callback`:加载完成后的回调,回调参数是对应模块的导出对象。 * **使用场景**:需要同步依赖、兼容老代码。 * **注意事项**:模块未加载成功时不会自动抛出异常,需在 callback 内自行处理。 * **示例**: 使用 ctx.requirejs 加载 lodash CDN 并结合 ctx.element 渲染 ```js const uid = ctx.model.uid; ctx.requirejs(['https://cdn.jsdelivr.net/npm/lodash@4/lodash.min.js'], function(_) { const arr = [1, 2, 3, 4, 5]; const shuffled = _.shuffle(arr); ctx.element.innerHTML = ` <div> <div>原数组: ${JSON.stringify(arr)}</div> <div id="shuffleResult_${uid}">乱序后: ${JSON.stringify(shuffled)}</div> <button id="reshuffleBtn_${uid}">重新乱序</button> </div> `; document.getElementById(`reshuffleBtn_${uid}`).onclick = () => { const newShuffled = _.shuffle(arr); document.getElementById(`shuffleResult_${uid}`).innerText = `乱序后: ${JSON.stringify(newShuffled)}`; }; }); ``` ### `ctx.requireAsync(modules: string | string[]): Promise<any>` * **说明**:异步加载外部 JavaScript 库,基于 RequireJS 封装,支持 `async/await`。推荐用于现代开发,代码更简洁。该方法会自动处理依赖关系,加载失败时会抛出异常。 * **参数**: * `modules`:单个模块名字符串或模块名数组。 * **返回**: * 一个 `Promise`,resolve 对应模块的导出对象。 * **使用场景**:推荐用于现代异步开发。 * **注意事项**:加载失败时会抛出异常,建议使用 try/catch 捕获。 * **示例**: 使用 ctx.requireAsync 异步加载 lodash CDN 并结合 ctx.element 渲染 ```js const uid = ctx.model.uid; const _ = await ctx.requireAsync('https://cdn.jsdelivr.net/npm/lodash@4/lodash.min.js'); const arr = [1, 2, 3, 4, 5]; const shuffled = _.shuffle(arr); ctx.element.innerHTML = ` <div> <div>原数组: ${JSON.stringify(arr)}</div> <div id="shuffleResult_${uid}">乱序后: ${JSON.stringify(shuffled)}</div> <button id="reshuffleBtn_${uid}">重新乱序</button> </div> `; document.getElementById(`reshuffleBtn_${uid}`).onclick = () => { const newShuffled = _.shuffle(arr); document.getElementById(`shuffleResult_${uid}`).innerText = `乱序后: ${JSON.stringify(newShuffled)}`; }; ``` ### `ctx.loadCSS(url: string): Promise<void>` * **说明**:异步加载外部 CSS 样式文件。该方法会在页面动态插入 `<link>` 标签,并自动处理重复加载和加载失败的情况。适合按需引入主题、第三方样式等。 * **参数**: * `url`:CSS 文件的完整 URL。 * **使用场景**:需要动态切换主题、按需加载样式。 * **注意事项**:建议确保 URL 可访问,避免跨域问题。 * **示例**: ```js // 加载 frappe-gantt 的 CSS 和 JS(使用 jsdelivr CDN) await ctx.loadCSS('https://cdn.jsdelivr.net/npm/frappe-gantt@0.5.0/dist/frappe-gantt.css'); await ctx.requireAsync('https://cdn.jsdelivr.net/npm/frappe-gantt@0.5.0/dist/frappe-gantt.min.js'); // 随机生成任务数据 function randomDate(start, days) { const date = new Date(start); date.setDate(date.getDate() + days); return date.toISOString().slice(0, 10); } const baseDate = new Date().toISOString().slice(0, 10); const tasks = Array.from({ length: 3 }).map((_, i) => { const startOffset = i === 0 ? 0 : i * 5; return { id: `Task ${i + 1}`, name: `阶段${i + 1}`, start: randomDate(baseDate, startOffset), end: randomDate(baseDate, startOffset + 4), progress: Math.floor(Math.random() * 100), dependencies: i === 0 ? '' : `Task ${i}` }; }); // 渲染 Gantt 图到 ctx.element const gantt = new Gantt(ctx.element, tasks, { view_mode: 'Day', date_format: 'YYYY-MM-DD', on_click: task => { alert(`点击了任务:${task.name}`); }, on_date_change: (task, start, end) => { console.log(`任务 "${task.name}" 改变时间:`, start, end); } }); ctx.model.on('destroy', () => { ctx.element.innerHTML = ''; }); ``` ### `ctx.getModelById(uid: string): FlowModel | null` * **说明**:根据唯一 ID 获取其他 Model 实例。该方法用于区块间通信和数据联动,返回目标区块的 model 实例引用。若目标 model 尚未初始化或不存在,则返回 null。 * **参数**: * `uid`:目标 Model 的唯一标识符。 * **返回**: * 对应的 `FlowModel` 实例,若不存在则返回 `null`。 * **使用场景**:多个区块间需要共享或联动数据时。 * **注意事项**:目标 Model 必须已初始化。 * **示例**: 获取图表区块,并重渲染。 ```ts const uid = ctx.model.uid; ctx.element.innerHTML = ` <button id="rerenderBtn_${uid}"> 重新渲染 ECharts 图表 </button> `; document.getElementById(`rerenderBtn_${uid}`).onclick = () => { const model = ctx.getModelById('33c11bb4298'); // 33c11bb4298 为上文 echarts 图表 if (model) { model.rerender(); } else { alert('未找到目标图表区块'); } }; ``` ### `ctx.request(options: AxiosRequestConfig): Promise<Response>` * **类型**:`(options: AxiosRequestConfig) => Promise<Response>` * **说明**: `ctx.request` 是基于 [axios](https://axios-http.com/) 的请求方法,自动集成了 NocoBase 的鉴权机制。你可以像使用 axios 一样发起任意 HTTP 请求,支持所有 axios 的配置参数(如 `url`、`method`、`params`、`data`、`headers` 等)。适用于需要自定义接口调用、一次性数据请求或不适合用 resource 管理的场景。 * **使用场景**: - 直接调用后端 API,获取或提交数据。 - 需要自定义请求参数、请求头或特殊接口调用。 - 适合一次性请求或不需要响应式联动的场景。 * **注意事项**: - 如果是 NocoBase 的数据源的数据表管理,推荐使用 `ctx.model.resource` 操作数据,只有在特殊或自定义接口场景下才使用 `ctx.request`。 - 返回值为 Promise,resolve 为服务器响应数据,reject 为请求异常。 - 已自动带上当前用户的鉴权信息,无需手动处理 token。 * **示例**: ```js try { const res = await ctx.request({ url: '/users', method: 'get', params: { page: 1, pageSize: 10 } }); console.log('用户列表', res); } catch (error) { console.error('请求失败', error); } ``` --- ## 常见问题解答(FAQ) **Q: 多个区块之间 getElementById 冲突怎么办?** A: 建议都加上 model.uid 后缀。 ```ts const uid = ctx.model.uid; ctx.element.innerHTML = ` <button id="rerenderBtn_${uid}"> 重新渲染 ECharts 图表 </button> `; ``` **Q: 如何实现区块间数据联动?** A: 通过 `getModelById` 获取其他区块的 model 实例,监听其数据变化或调用其方法。 **Q: ctx.request 和 ctx.model.resource 的区别?** A: - `ctx.request` 是底层的 HTTP 请求方法,直接发起 API 调用,适合简单、一次性的接口请求,返回原始响应数据,需要手动处理数据结构、状态和错误。 - `ctx.model.resource` 是基于资源模型的高级数据操作对象,封装了常用的增删改查(CRUD)、分页、筛选、缓存等能力,并自动与区块的 model 联动,适合需要和后端数据表/资源持续交互、响应式更新的场景。 一般推荐优先使用 `ctx.resource` 管理数据,只有在特殊或自定义接口场景下才使用 `ctx.request`。 **Q: ctx.requireAsync 和 ctx.requirejs 的区别?** A: - `ctx.requirejs` 是开源 [requirejs](https://requirejs.org/) 库的加载接口,基于回调函数风格。它用于动态加载外部 JavaScript 模块或 CDN 脚本,适合需要兼容 requirejs 生态或老代码的场景。它本身与“同步/异步”无关,加载过程依然是异步的,只是通过回调获取结果。 - `ctx.requireAsync` 是基于 requirejs 封装的 Promise 风格异步方法,支持 `async/await`,推荐用于现代 JavaScript 开发。加载失败会抛出异常,代码更简洁,易于错误处理。 **推荐优先使用 `ctx.requireAsync`,只有在必须兼容 requirejs 回调风格时才考虑 `ctx.requirejs`。** **Q: 如果低代码区块的 JavaScript 代码有问题导致页面崩溃无法打开怎么办?** A: 可以在 URL 末尾添加 `skip_nocobase_lowcode=true` 参数来跳过低代码区块的执行。例如:`http://localhost:3000/admin?skip_nocobase_lowcode=true`。这样可以避免有问题的 JavaScript 代码(或未来版本的破坏性变更)导致页面崩溃而无法从 UI 界面恢复。进入页面后可以修复或删除有问题的低代码区块,然后移除该 URL 参数恢复正常使用。