From 1c829e0a578f9143e269d0c11df6289b022af6af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E5=A4=9A=E7=9B=8A?= Date: Fri, 29 Jan 2021 19:35:15 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E6=8D=A2=20github=20=E7=9A=84=E9=93=BE?= =?UTF-8?q?=E6=8E=A5=20(#1489)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh-CN/components/icon.md | 2 +- docs/zh-CN/index.md | 2 +- docs/zh-CN/start/faq.md | 2 +- docs/zh-CN/start/getting-started.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/zh-CN/components/icon.md b/docs/zh-CN/components/icon.md index 5475d3f74..605f8e36e 100755 --- a/docs/zh-CN/components/icon.md +++ b/docs/zh-CN/components/icon.md @@ -8,7 +8,7 @@ icon: order: 50 --- -> 在 React 项目中使用 Icon 需要引入 `font-awesome@4.7.0`,然后在代码中 `import 'font-awesome/css/font-awesome.css'`,还有相关的 webpack 配置,具体请参考 [amis-react-starter](https://github.com/fex-team/amis-react-starter) 里的配置 +> 在 React 项目中使用 Icon 需要引入 `font-awesome@4.7.0`,然后在代码中 `import 'font-awesome/css/font-awesome.css'`,还有相关的 webpack 配置,具体请参考 [amis-react-starter](https://github.com/aisuda/amis-react-starter) 里的配置 ## 基本使用 diff --git a/docs/zh-CN/index.md b/docs/zh-CN/index.md index fc47dcaf6..360dc2e3f 100644 --- a/docs/zh-CN/index.md +++ b/docs/zh-CN/index.md @@ -200,7 +200,7 @@ amis 是一个低代码前端框架,它使用 JSON 配置来生成页面,可 - **不需要懂前端**:在百度内部,大部分 amis 用户之前从来没写过前端页面,也不会 `JavaScript`,却能做出专业且复杂的后台界面,这是所有其他前端 UI 库都无法做到的; - **不受前端技术更新的影响**:百度内部最老的 amis 页面是 4 年多前创建的,至今还在使用,而当年的 `Angular/Vue/React` 版本现在都废弃了,当年流行的 `Gulp` 也被 `Webpack` 取代了,如果这些页面不是用 amis,现在的维护成本会很高; - **享受 amis 的不断升级**:amis 一直在提升细节交互体验,比如表格首行冻结、下拉框大数据下不卡顿等,之前的 JSON 配置完全不需要修改; -- 可以 **完全** 使用 [可视化页面编辑器](https://fex-team.github.io/amis-editor-demo/) 来制作页面:一般前端可视化编辑器只能用来做静态原型,而 amis 可视化编辑器做出的页面是可以直接上线的。 +- 可以 **完全** 使用 [可视化页面编辑器](https://aisuda.github.io/amis-editor-demo/) 来制作页面:一般前端可视化编辑器只能用来做静态原型,而 amis 可视化编辑器做出的页面是可以直接上线的。 ## amis 的其它亮点 diff --git a/docs/zh-CN/start/faq.md b/docs/zh-CN/start/faq.md index 86a94fb58..417d732fe 100644 --- a/docs/zh-CN/start/faq.md +++ b/docs/zh-CN/start/faq.md @@ -4,7 +4,7 @@ title: 常见问题 ## 如何实现左侧导航栏页面跳转? -在 1.1.1 之后的版本提供了新的 app 组件,可以基于它实现导航功能,请参考 `https://github.com/fex-team/amis-admin` 项目。 +在 1.1.1 之后的版本提供了新的 app 组件,可以基于它实现导航功能,请参考 `https://github.com/aisuda/amis-admin` 项目。 另外 amis 团队还开发了「[爱速搭](http://suda.baidu.com/)」,即便完全不懂前端也能基于它开发应用。 diff --git a/docs/zh-CN/start/getting-started.md b/docs/zh-CN/start/getting-started.md index 8dd517bdc..80dfe2416 100644 --- a/docs/zh-CN/start/getting-started.md +++ b/docs/zh-CN/start/getting-started.md @@ -211,7 +211,7 @@ amis.embed( ## react -初始项目请参考 。 +初始项目请参考 。 如果在已有项目中,React 版本需要是 `^16.8.6`,mobx 需要 `^4.5.0`。 From b799ca8a05706c796d84f445256a0ec9f27b0433 Mon Sep 17 00:00:00 2001 From: RickCole Date: Fri, 29 Jan 2021 23:10:46 +0800 Subject: [PATCH 2/6] =?UTF-8?q?tabs=20title=20=E6=94=AF=E6=8C=81=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=20(#1484)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderers/Tabs.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/renderers/Tabs.tsx b/src/renderers/Tabs.tsx index a86fcbd6d..fa0f1d502 100644 --- a/src/renderers/Tabs.tsx +++ b/src/renderers/Tabs.tsx @@ -13,6 +13,7 @@ import { SchemaIcon } from '../Schema'; import {ActionSchema} from './Action'; +import {filter} from '../utils/tpl'; export interface TabSchema extends Omit { /** @@ -323,6 +324,7 @@ export default class Tabs extends React.Component { isVisible(tab, data) ? ( Date: Fri, 29 Jan 2021 23:11:10 +0800 Subject: [PATCH 3/6] =?UTF-8?q?=E6=80=BB=E7=BB=93=E8=A1=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=20(#1485)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderers/Table/TableBody.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/renderers/Table/TableBody.tsx b/src/renderers/Table/TableBody.tsx index 2dc7809a0..abfa33e0c 100644 --- a/src/renderers/Table/TableBody.tsx +++ b/src/renderers/Table/TableBody.tsx @@ -197,9 +197,15 @@ export class TableBody extends React.Component { result[0].colSpan = (result[0].colSpan || 1) + 1; } + // 如果是展开栏,让它和下一列合并。 + if (columns[0].type === '__expandme' && result[0]) { + result[0].colSpan = (result[0].colSpan || 1) + 1; + } + // 缺少的单元格补齐 const appendLen = - filterColumns.length - result.reduce((p, c) => p + (c.colSpan || 1), 0); + columns.length - result.reduce((p, c) => p + (c.colSpan || 1), 0); + if (appendLen) { const item = result.pop(); result.push({ From 3429e8487e7caea2dcb8e8c2c12d2ffbc74dd29a Mon Sep 17 00:00:00 2001 From: RickCole Date: Fri, 29 Jan 2021 23:12:20 +0800 Subject: [PATCH 4/6] =?UTF-8?q?Tree-Select=20Label=E4=BF=AE=E5=A4=8D=20(#1?= =?UTF-8?q?486)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Tree.tsx | 12 +++++++++--- src/renderers/Form/TreeSelect.tsx | 9 +++++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/components/Tree.tsx b/src/components/Tree.tsx index a63c1f0d6..a39625051 100644 --- a/src/components/Tree.tsx +++ b/src/components/Tree.tsx @@ -804,15 +804,21 @@ export class TreeSelector extends React.Component< export function findAncestorsWithValue( ancestors: any[], options: any[], - value: any + value: any, + valueField = 'value' ) { for (let option of options) { - if (option.value === value) { + if (option[valueField] === value) { return true; } // 如果没有就在 children 中查找 if (option.children) { - const inChild = findAncestorsWithValue(ancestors, option.children, value); + const inChild = findAncestorsWithValue( + ancestors, + option.children, + value, + valueField + ); if (inChild) { ancestors.unshift(option); return true; diff --git a/src/renderers/Form/TreeSelect.tsx b/src/renderers/Form/TreeSelect.tsx index f6b536b95..ab1294a3d 100644 --- a/src/renderers/Form/TreeSelect.tsx +++ b/src/renderers/Form/TreeSelect.tsx @@ -410,10 +410,15 @@ export default class TreeSelectControl extends React.Component< @autobind renderItem(item: Option) { - const {labelField} = this.props; + const {labelField, valueField} = this.props; // 将所有祖先节点也展现出来 const ancestors: any[] = []; - findAncestorsWithValue(ancestors, this.props.options, item.value); + findAncestorsWithValue( + ancestors, + this.props.options, + item[valueField || 'value'], + valueField + ); let ancestorsLabel = ''; if (ancestors.length) { ancestorsLabel = From cdfad04691a5c502d3133fff4ebc42a349403909 Mon Sep 17 00:00:00 2001 From: RickCole Date: Fri, 29 Jan 2021 23:12:40 +0800 Subject: [PATCH 5/6] =?UTF-8?q?precision=20=E6=94=AF=E6=8C=81=E5=8F=98?= =?UTF-8?q?=E9=87=8F=20(#1488)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderers/Form/Number.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/renderers/Form/Number.tsx b/src/renderers/Form/Number.tsx index 7454a5a35..369f2f77f 100644 --- a/src/renderers/Form/Number.tsx +++ b/src/renderers/Form/Number.tsx @@ -92,8 +92,9 @@ export default class NumberControl extends React.Component { let precisionProps: any = {}; - if (typeof precision === 'number') { - precisionProps.precision = precision; + const finalPrecision = this.filterNum(precision); + if (typeof finalPrecision === 'number') { + precisionProps.precision = finalPrecision; } return ( @@ -106,7 +107,7 @@ export default class NumberControl extends React.Component { onChange={this.handleChange} disabled={disabled} placeholder={placeholder} - precision={precision} + precision={finalPrecision} showSteps={showSteps} /> From 60d4a386702c60b0c29a8ffd9cf830c2c418edcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E5=A4=9A=E7=9B=8A?= Date: Fri, 29 Jan 2021 23:13:24 +0800 Subject: [PATCH 6/6] =?UTF-8?q?feat=EF=BC=9Aservice=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=20WebSocket=20=E5=AE=9E=E6=97=B6=E6=9B=B4=E6=96=B0=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=20(#1487)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh-CN/components/form/service.md | 4 ++ docs/zh-CN/components/service.md | 67 ++++++++++++++++++++++++++- src/factory.tsx | 24 ++++++++++ src/renderers/Service.tsx | 23 +++++++++ src/store/service.ts | 24 ++++++++++ 5 files changed, 141 insertions(+), 1 deletion(-) diff --git a/docs/zh-CN/components/form/service.md b/docs/zh-CN/components/form/service.md index 849185d29..14cbe57ac 100755 --- a/docs/zh-CN/components/form/service.md +++ b/docs/zh-CN/components/form/service.md @@ -192,3 +192,7 @@ Service 中的`api`和`schemaApi`都支持**接口联动**。 } } ``` + +## 通过 WebSocket 实时更新数据 + +请参考 [service](../../service) 中的内容。 diff --git a/docs/zh-CN/components/service.md b/docs/zh-CN/components/service.md index 7dba4bb37..79c9f4c6a 100755 --- a/docs/zh-CN/components/service.md +++ b/docs/zh-CN/components/service.md @@ -281,7 +281,71 @@ amis 中部分组件,作为展示组件,自身没有**使用接口初始化 ## 定时轮询刷新 -设置 `interval` 可以定时刷新,单位是毫秒,最小间隔是 1 秒。 +设置 `interval` 可以定时刷新 api 接口,单位是毫秒,最小间隔是 1 秒。 + +## 通过 WebSocket 实时获取数据 + +Service 支持通过 WebSocket 获取数据,只需要设置 ws(由于无示例服务,所以无法在线演示)。 + +```json +{ + "type": "service", + "api": "https://3xsw4ap8wah59.cfc-execute.bj.baidubce.com/api/amis-mock/mock2/page/initData", + "ws": "ws://localhost:8777", + "body": { + "type": "panel", + "title": "$title", + "body": "随机数:${random}" + } +} +``` + +可以只设置 ws,通过 ws 来获取所有数据,也可以同时设置 api 和 ws,让 api 用于获取全部数据,而 ws 用于获取实时更新的数据。 + +后端实现示例,基于 [ws](https://github.com/websockets/ws): + +```javascript +const WebSocket = require('ws'); + +const ws = new WebSocket.Server({port: 8777}); + +ws.on('connection', function connection(ws) { + setInterval(() => { + const random = Math.floor(Math.random() * Math.floor(100)); + // 返回给 amis 的数据 + const data = { + random + }; + // 发送前需要转成字符串 + ws.send(JSON.stringify(data)); + }, 500); +}); +``` + +WebSocket 客户端的默认实现是使用标准 WebSocket,如果后端使用定制的 WebSocket,比如 socket.io,可以通过覆盖 `env.wsFetcher` 来自己实现数据获取方法,默认实现是: + +```javascript +wsFetcher(ws, onMessage, onError) { + if (ws) { + const socket = new WebSocket(ws); + socket.onmessage = (event: any) => { + if (event.data) { + onMessage(JSON.parse(event.data)); + } + }; + socket.onerror = onError; + return { + close: socket.close + }; + } else { + return { + close: () => {} + }; + } +} +``` + +通过 onMessage 来通知 amis 数据修改了,并返回 close 函数来关闭连接。 ## 属性表 @@ -291,6 +355,7 @@ amis 中部分组件,作为展示组件,自身没有**使用接口初始化 | className | `string` | | 外层 Dom 的类名 | | body | [SchemaNode](../types/schemanode) | | 内容容器 | | api | [api](../types/api) | | 初始化数据域接口地址 | +| ws | `string` | | WebScocket 地址 | | initFetch | `boolean` | | 是否默认拉取 | | schemaApi | [api](../types/api) | | 用来获取远程 Schema 接口地址 | | initFetchSchema | `boolean` | | 是否默认拉取 Schema | diff --git a/src/factory.tsx b/src/factory.tsx index aa5286f0e..d4b958f26 100644 --- a/src/factory.tsx +++ b/src/factory.tsx @@ -123,6 +123,11 @@ export interface RenderSchemaFilter { export interface RenderOptions { session?: string; fetcher?: (config: fetcherConfig) => Promise; + wsFetcher?: ( + ws: string, + onMessage: (data: any) => void, + onError: (error: any) => void + ) => void; isCancel?: (value: any) => boolean; notify?: ( type: 'error' | 'success', @@ -266,6 +271,25 @@ const defaultOptions: RenderOptions = { fetcher() { return Promise.reject('fetcher is required'); }, + // 使用 WebSocket 来实时获取数据 + wsFetcher(ws, onMessage, onError) { + if (ws) { + const socket = new WebSocket(ws); + socket.onmessage = (event: any) => { + if (event.data) { + onMessage(JSON.parse(event.data)); + } + }; + socket.onerror = onError; + return { + close: socket.close + }; + } else { + return { + close: () => {} + }; + } + }, isCancel() { console.error( 'Please implements this. see https://baidu.gitee.io/amis/docs/start/getting-started#%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97' diff --git a/src/renderers/Service.tsx b/src/renderers/Service.tsx index d5e1c265e..4e7357f70 100644 --- a/src/renderers/Service.tsx +++ b/src/renderers/Service.tsx @@ -34,6 +34,11 @@ export interface ServiceSchema extends BaseSchema { */ api?: SchemaApi; + /** + * WebScocket 地址,用于实时获取数据 + */ + ws?: string; + /** * 内容区域 */ @@ -100,6 +105,9 @@ export default class Service extends React.Component { timer: NodeJS.Timeout; mounted: boolean; + // 主要是用于关闭 socket + socket: any; + static defaultProps: Partial = { messages: { fetchFailed: 'fetchFailed' @@ -151,11 +159,21 @@ export default class Service extends React.Component { errorMessage: fetchFailed }) .then(this.afterSchemaFetch); + + if (props.ws && prevProps.ws !== props.ws) { + if (this.socket) { + this.socket.close(); + } + this.socket = store.fetchWSData(props.ws, this.afterDataFetch); + } } componentWillUnmount() { this.mounted = false; clearTimeout(this.timer); + if (this.socket && this.socket.close) { + this.socket.close(); + } } @autobind @@ -164,6 +182,7 @@ export default class Service extends React.Component { schemaApi, initFetchSchema, api, + ws, initFetch, initFetchOn, store, @@ -187,6 +206,10 @@ export default class Service extends React.Component { }) .then(this.afterDataFetch); } + + if (ws) { + this.socket = store.fetchWSData(ws, this.afterDataFetch); + } } afterDataFetch(data: any) { diff --git a/src/store/service.ts b/src/store/service.ts index 5b8f40742..924f8291c 100644 --- a/src/store/service.ts +++ b/src/store/service.ts @@ -140,6 +140,28 @@ export const ServiceStore = iRendererStore } }); + const fetchWSData = (ws: string, afterDataFetch: (data: any) => any) => { + const env = getEnv(self); + + env.wsFetcher( + ws, + (data: any) => { + self.updateData(data, undefined, false); + self.setHasRemoteData(); + // 因为 WebSocket 只会获取纯数据,所以没有 msg 之类的 + afterDataFetch({ok: true, data: data}); + }, + (error: any) => { + updateMessage(error, true); + env.notify('error', error); + } + ); + }; + + const setHasRemoteData = () => { + self.hasRemoteData = true; + }; + const fetchData: ( api: Api, data?: object, @@ -468,9 +490,11 @@ export const ServiceStore = iRendererStore markBusying, fetchInitData, fetchData, + fetchWSData, reInitData, updateMessage, clearMessage, + setHasRemoteData, saveRemote, fetchSchema, checkRemote