Merge branch 'master' into feat-BCE-FE-1706

This commit is contained in:
hsm-lv 2023-07-10 10:31:25 +08:00 committed by GitHub
commit 9068aa2afd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
621 changed files with 13370 additions and 2810 deletions

View File

@ -10,5 +10,9 @@ insert_final_newline = true
indent_style = space
indent_size = 2
[**.{py}]
indent_style = space
indent_size = 4
[*.md]
trim_trailing_whitespace = false

View File

@ -28,7 +28,7 @@ jobs:
- name: build gh-pages
run: |
npm i --legacy-peer-deps
cd packages/ooxml-viewer
cd packages/office-viewer
npm i --legacy-peer-deps
cd ../../
npm run build --workspaces

View File

@ -27,7 +27,7 @@ jobs:
NODE_OPTIONS: '--max-old-space-size=8192'
run: |
npm i --legacy-peer-deps
cd packages/ooxml-viewer
cd packages/office-viewer
npm i --legacy-peer-deps
cd ../../
npm run build --workspace amis-formula
@ -44,7 +44,7 @@ jobs:
- name: test
run: |
npm test --workspaces
cd packages/ooxml-viewer
cd packages/office-viewer
npm test
cd ../../
sh deploy-gh-pages.sh

24
.gitpod.yml Normal file
View File

@ -0,0 +1,24 @@
# This configuration file was automatically generated by Gitpod.
# Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml)
# and commit this file to your remote git repository to share the goodness with others.
# Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart
ports:
- port: 3000
onOpen: ignore
visibility: public
- port: 8888
onOpen: ignore
visibility: public
tasks:
- name: SDK
command: |
npm i
npm run build --workspace amis --workspace amis-ui
npx -y serve packages/amis/
- name: Amis
command: |
gp await-port 3000
npm run start

View File

@ -17,7 +17,7 @@
"path": "./packages/amis"
},
{
"path": "./packages/ooxml-viewer"
"path": "./packages/office-viewer"
}
]
}

View File

@ -174,7 +174,7 @@ icon 也可以是 url 地址,比如
## 操作前确认
可以通过配置`confirmText`,实现在任意操作前,弹出提示框确认是否进行该操作。
可以通过配置`confirmText`,实现在任意操作前,弹出提示框确认是否进行该操作。同时可以通过配置 `confirmTitle` 来设置弹窗标题
```schema: scope="body"
{
@ -182,6 +182,7 @@ icon 也可以是 url 地址,比如
"type": "button",
"actionType": "ajax",
"confirmText": "确认要发出这个请求?",
"confirmTitle": "炸弹",
"api": "/api/mock2/form/saveForm"
}
```
@ -1029,6 +1030,7 @@ action 还可以使用 `body` 来渲染其他组件,让那些不支持行为
| activeClassName | `string` | `is-active` | 给按钮高亮添加类名。 |
| block | `boolean` | - | 用`display:"block"`来显示按钮。 |
| confirmText | [模板](../../docs/concepts/template) | - | 当设置后,操作在开始前会询问用户。可用 `${xxx}` 取值。 |
| confirmTitle | [模板](../../docs/concepts/template) | - | 确认框标题,前提是 confirmText 有内容,支持模版语法 |
| reload | `string` | - | 指定此次操作完后,需要刷新的目标组件名字(组件的`name`值,自己配置的),多个请用 `,` 号隔开。 |
| tooltip | `string` | - | 鼠标停留时弹出该段文字,也可以配置对象类型:字段为`title`和`content`。可用 `${xxx}` 取值。 |
| disabledTip | `'string' \| 'TooltipObject'` | - | 被禁用后鼠标停留时弹出该段文字,也可以配置对象类型:字段为`title`和`content`。可用 `${xxx}` 取值。 |

View File

@ -213,23 +213,144 @@ order: 36
## CollapseGroup 属性表
| 属性名 | 类型 | 默认值 | 说明 |
| - | - | - | - |
| type | `string` | `"collapse-group"` | 指定为 collapse-group 渲染器 |
| activeKey | `Array<string \| number \| never> \| string \| number` | - | 初始化激活面板的key |
| accordion | `boolean` | `false` | 手风琴模式 |
| expandIcon | `SchemaNode` | - | 自定义切换图标 |
| expandIconPosition | `string` | `"left"` | 设置图标位置,可选值`left \| right` |
| 属性名 | 类型 | 默认值 | 说明 |
| ------------------ | ------------------------------------------------------ | ------------------ | ----------------------------------- |
| type | `string` | `"collapse-group"` | 指定为 collapse-group 渲染器 |
| activeKey | `Array<string \| number \| never> \| string \| number` | - | 初始化激活面板的 key |
| accordion | `boolean` | `false` | 手风琴模式 |
| expandIcon | `SchemaNode` | - | 自定义切换图标 |
| expandIconPosition | `string` | `"left"` | 设置图标位置,可选值`left \| right` |
## CollapseGroup 事件表
当前组件会对外派发以下事件,可以通过 onEvent 来监听这些事件,并通过 actions 来配置执行的动作,在 actions 中可以通过${事件参数名}或${event.data.[事件参数名]}来获取事件产生的数据,详细查看事件动作。
| 事件名称 | 事件参数 | 说明 |
| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- |
| change | `activeKeys: Array<string \| number>` 当前展开的索引列表 <br /> `collapseId: string \| number` 折叠器索引 <br/> `collapsed: boolean` 折叠器状态 | 折叠面板折叠状态改变时触发 |
### change
折叠面板折叠状态改变时触发。
```schema: scope="body"
{
"type": "collapse-group",
"activeKey": [
"1"
],
"body": [
{
"type": "collapse",
"key": "1",
"active": true,
"header": "标题1",
"body": [
{
"type": "tpl",
"tpl": "这里是内容1",
"wrapperComponent": "",
"inline": false,
"id": "u:757ad799da08"
}
],
"id": "u:b1b68dfbb08d"
},
{
"type": "collapse",
"key": "2",
"header": "标题2",
"body": [
{
"type": "tpl",
"tpl": "这里是内容1",
"wrapperComponent": "",
"inline": false,
"id": "u:92caa03f227e"
}
],
"id": "u:621a22c8b18c"
}
],
"id": "u:23e4c5ec9c89",
"onEvent": {
"change": {
"weight": 0,
"actions": [
{
"actionType": "toast",
"args": {
"msgType": "info",
"position": "top-right",
"closeButton": true,
"showIcon": true,
"title": "折叠状态改变",
"msg": "activeKeys: ${event.data.activeKeys | json}, collapseId: ${event.data.collapseId}, collapsed: ${event.data.collapsed}",
"className": "theme-toast-action-scope"
}
}
]
}
}
}
```
## Collapse 属性表
| 属性名 | 类型 | 默认值 | 说明 |
| - | - | - | - |
| type | `string` | `"collapse"` | 指定为 collapse 渲染器 |
| disabled | `boolean` | `false` | 禁用 |
| collapsed | `boolean` | `true` | 初始状态是否折叠 |
| key | `string \| number` | - | 标识 |
| header | `string \| SchemaNode` | - | 标题 |
| body | `string \| SchemaNode` | - | 内容 |
| showArrow | `boolean` | `true` | 是否展示图标 |
| 属性名 | 类型 | 默认值 | 说明 |
| --------- | ---------------------- | ------------ | ---------------------- |
| type | `string` | `"collapse"` | 指定为 collapse 渲染器 |
| disabled | `boolean` | `false` | 禁用 |
| collapsed | `boolean` | `true` | 初始状态是否折叠 |
| key | `string \| number` | - | 标识 |
| header | `string \| SchemaNode` | - | 标题 |
| body | `string \| SchemaNode` | - | 内容 |
| showArrow | `boolean` | `true` | 是否展示图标 |
## Collapse 事件表
当前组件会对外派发以下事件,可以通过 onEvent 来监听这些事件,并通过 actions 来配置执行的动作,在 actions 中可以通过${事件参数名}或${event.data.[事件参数名]}来获取事件产生的数据,详细查看事件动作。
| 事件名称 | 事件参数 | 说明 |
| -------- | ------------------------------- | ------------------------ |
| change | `collapsed: boolean` 折叠器状态 | 折叠器折叠状态改变时触发 |
### change
折叠面板折叠状态改变时触发。
```schema: scope="body"
{
"type": "collapse",
"header": "标题",
"body": [
{
"type": "tpl",
"tpl": "内容",
"wrapperComponent": "",
"inline": false,
"id": "u:6588c12ee3b0"
}
],
"id": "u:62aa2f0c7fd9",
"onEvent": {
"change": {
"weight": 0,
"actions": [
{
"actionType": "toast",
"args": {
"msgType": "info",
"position": "top-right",
"closeButton": true,
"showIcon": true,
"title": "collapsedChange",
"msg": "collapsed: ${event.data.collapsed}",
"className": "theme-toast-action-scope"
}
}
]
}
}
}
```

View File

@ -2183,6 +2183,60 @@ crud 组件支持通过配置`headerToolbar`和`footerToolbar`属性,实现在
}
```
#### 指定导出行
> 3.2.0 及以上版本
可以通过配置 `rowSlice` 属性来控制导出哪些行
```schema: scope="body"
{
"type": "crud",
"syncLocation": false,
"api": "/api/mock2/sample",
"headerToolbar": [{
"type": "export-excel",
"label": "导出 1, 4, 5 行",
"rowSlice": "0,3:5"
}],
"columns": [
{
"name": "id",
"label": "ID"
},
{
"name": "engine",
"label": "Rendering engine"
},
{
"name": "browser",
"label": "Browser"
},
{
"name": "platform",
"label": "Platform(s)"
},
{
"name": "version",
"label": "Engine version"
},
{
"name": "grade",
"label": "CSS grade"
}
]
}
```
`rowSlice` 支持以下写法
- 取单个值 '1,2,3',代表取 1、2、3 索引的内容
- 取范围 '3:10',代表取 3-9 索引的内容
- ':' 代表所有行
- '1:' 代表从第二行开始到结束
- 结束可以是负数 ':-1',代表除了最后一个元素的所有元素,开始为空代表 0
- 前两种的组合 '1,3:10',代表取 1 索引和 3-9 索引的内容
#### 通过 api 导出 Excel
> 1.1.6 以上版本支持

View File

@ -994,6 +994,102 @@ selectMode 为`chained`时,使用`source`字段
}
```
## 拖拽控制
当`draggable`为`false`时,关闭拖拽
```schema: scope="body"
{
"type": "form",
"debug": true,
"body": [
{
"type": "condition-builder",
"label": "条件组件",
"title": "条件组合设置",
"draggable": false,
"name": "conditions",
"description": "适合让用户自己拼查询条件,然后后端根据数据生成 query where",
"pickerIcon": {
"type": "icon",
"icon": "edit",
"className": "w-4 h-4"
},
"fields": [
{
"label": "文本",
"type": "text",
"name": "text"
},
{
"label": "数字",
"type": "number",
"name": "number"
},
{
"label": "布尔",
"type": "boolean",
"name": "boolean"
},
{
"label": "选项",
"type": "select",
"name": "select",
"options": [
{
"label": "A",
"value": "a"
},
{
"label": "B",
"value": "b"
},
{
"label": "C",
"value": "c"
},
{
"label": "D",
"value": "d"
},
{
"label": "E",
"value": "e"
}
]
},
{
"label": "动态选项",
"type": "select",
"name": "select2",
"source": "/api/mock2/form/getOptions?waitSeconds=1"
},
{
"label": "日期",
"children": [
{
"label": "日期",
"type": "date",
"name": "date"
},
{
"label": "时间",
"type": "time",
"name": "time"
},
{
"label": "日期时间",
"type": "datetime",
"name": "datetime"
}
]
}
]
}
]
}
```
## 属性表
| 属性名 | 类型 | 默认值 | 说明 |
@ -1006,6 +1102,7 @@ selectMode 为`chained`时,使用`source`字段
| fields | | | 字段配置 |
| showANDOR | `boolean` | | 用于 simple 模式下显示切换按钮 |
| showNot | `boolean` | | 是否显示「非」按钮 |
| draggable | `boolean` | true | 是否可拖拽 |
| searchable | `boolean` | | 字段是否可搜索 |
| selectMode | `'list'` \| `'tree'` \| `'chained'` | `'list'` | 组合条件左侧选项类型。`'chained'`模式需要`3.2.0及以上版本` |
| addBtnVisibleOn | `string` | | 表达式:控制按钮“添加条件”的显示。参数为`depth`、`breadth`,分别代表深度、长度。表达式需要返回`boolean`类型`3.2.0及以上版本` |

View File

@ -1469,13 +1469,402 @@ Form 支持轮询初始化接口,步骤如下:
当前组件对外暴露以下特性动作,其他组件可以通过指定`actionType: 动作名称`、`componentId: 该组件id`来触发这些动作,动作配置可以通过`args: {动作配置项名称: xxx}`来配置具体的参数,详细请查看[事件动作](../../docs/concepts/event-action#触发其他组件的动作)。
| 动作名称 | 动作配置 | 说明 |
| --------- | ------------------------------ | -------------------------- |
| submit | - | 提交表单 |
| reset | - | 重置表单 |
| clear | - | 清空表单 |
| validate | - | 校验表单 |
| reload | - | 刷新(重新加载) |
| setValue | `value: object` 更新的表单数据 | 更新数据,对数据进行 merge |
| static | - | 表单切换为静态展示 |
| nonstatic | - | 表单切换为普通输入态 |
| 动作名称 | 动作配置 | 说明 |
| --------- | --------------------------------------------------- | -------------------------- |
| validate | `outputVar: string` 校验结果,默认为 validateResult | 校验表单 |
| submit | `outputVar: string` 提交结果,默认为 submitResult | 提交表单 |
| setValue | `value: object` 更新的表单数据 | 更新数据,对数据进行 merge |
| reload | - | 刷新(重新加载) |
| reset | - | 重置表单 |
| clear | - | 清空表单 |
| static | - | 表单切换为静态展示 |
| nonstatic | - | 表单切换为普通输入态 |
### validate
校验结果默认缓存在`${event.data.validateResult}``true`表示校验成功,`false`表示检验失败。可以通过添加`outputVar`配置来修改缓存的变量。
校验结果的结构如下:
```json
{
// 是否成功。非空表示失败
"error": "依赖的部分字段没有通过验证",
// 表单项报错信息。key值为该表单项的name值
"errors": {
"email": ["Email 格式不正确"],
...
},
// 提交验证的表单数据
"payload": {
"name": "amis",
"email": "amis@baidu"
}
}
```
```schema: scope="body"
[
{
"type": "button",
"label": "校验表单",
className: "mb-2",
"onEvent": {
"click": {
"actions": [
{
"actionType": "validate",
"componentId": "form_validate",
"outputVar": "form_validate_result"
},
{
"actionType": "setValue",
"componentId": "validate_info",
"args": {
"value": "${event.data.form_validate_result|json}"
}
}
]
}
}
},
{
type: 'input-text',
name: 'validate_info',
id: 'validate_info',
label: '校验结果:',
static: true
},
{
"type": "form",
"id": "form_validate",
"api": "/api/mock2/form/saveForm",
"body": [
{
"type": "input-text",
"name": "name",
"label": "姓名:",
"required": true
},
{
"name": "email",
"type": "input-text",
"label": "邮箱:",
"required": true,
"validations": {
"isEmail": true
}
}
]
}
]
```
### submit
提交结果默认缓存在`${event.data.submitResult}`,可以通过添加`outputVar`配置来修改缓存的变量。
提交结果的结构如下:
```json
{
// 是否成功。是否成功。非空表示失败
"error": "依赖的部分字段没有通过验证",
// 错误信息。如果是校验失败则errors为表单项报错信息key值为该表单项的name值
"errors": {
...
},
// 提交的表单数据
"payload": {
"name": "amis",
"email": "amis@baidu.com"
},
// 提交请求返回的响应结果数据
"responseData": {
"id": "1"
}
}
```
```schema: scope="body"
[
{
"type": "button",
"label": "提交表单",
className: "mb-2",
"onEvent": {
"click": {
"actions": [
{
"actionType": "submit",
"componentId": "form_submit",
"outputVar": "form_submit_result"
},
{
"actionType": "setValue",
"componentId": "submit_info",
"args": {
"value": "${event.data.form_submit_result|json}"
}
}
]
}
}
},
{
type: 'input-text',
name: 'submit_info',
id: 'submit_info',
label: '提交结果:',
static: true
},
{
"type": "form",
"id": "form_submit",
"api": "/api/mock2/form/saveForm",
"body": [
{
"type": "input-text",
"name": "name",
"label": "姓名:",
"required": true
},
{
"name": "email",
"type": "input-text",
"label": "邮箱:",
"required": true,
"validations": {
"isEmail": true
}
}
]
}
]
```
### setValue
通过`setValue`来更新表单数据,其中`value`中的数据将和目标表单的数据做合并,即同名覆盖。
```schema: scope="body"
[
{
"type": "button",
"label": "修改表单数据",
className: "mb-2",
"onEvent": {
"click": {
"actions": [
{
"actionType": "setValue",
"componentId": "form_setvalue",
"args": {
"value": {
"name": "amis",
"email": "amis@baidu.com"
}
}
}
]
}
}
},
{
"type": "form",
"id": "form_setvalue",
"body": [
{
"type": "input-text",
"name": "name",
"label": "姓名:"
},
{
"name": "email",
"type": "input-text",
"label": "邮箱:"
}
]
}
]
```
### reload
通过`reload`来重新请求表单的初始化接口,实现表单刷新。
```schema: scope="body"
[
{
"type": "button",
"label": "刷新表单",
className: "mb-2",
"onEvent": {
"click": {
"actions": [
{
"actionType": "reload",
"componentId": "form_reload"
}
]
}
}
},
{
"type": "form",
"id": "form_reload",
"debug": true,
"initApi": "/api/mock2/form/initData",
"body": [
{
"type": "input-text",
"name": "name",
"label": "姓名:"
},
{
"name": "author",
"type": "input-text",
"label": "作者:"
}
]
}
]
```
### reset
通过`reset`将表单数据重置为初始数据,初始数据可以是静态数据或初始化接口返回的数据。
```schema: scope="body"
[
{
"type": "button",
"label": "重置表单",
className: "mb-2",
"onEvent": {
"click": {
"actions": [
{
"actionType": "reset",
"componentId": "form_reset"
}
]
}
}
},
{
"type": "form",
"id": "form_reset",
"initApi": "/api/mock2/form/initData",
"body": [
{
"type": "alert",
"body": "修改表单项的值,然后点击【重置表单】,表单数据将被重置为初始化数据"
},
{
"type": "input-text",
"name": "name",
"label": "姓名:"
},
{
"name": "author",
"type": "input-text",
"label": "作者:"
}
]
}
]
```
### clear
通过`clear`来清空表单中的表单项数据,不包含`hidden`类型、未绑定表单项的初始化数据字段。
```schema: scope="body"
[
{
"type": "button",
"label": "清空表单",
className: "mb-2",
"onEvent": {
"click": {
"actions": [
{
"actionType": "clear",
"componentId": "form_clear"
}
]
}
}
},
{
"type": "form",
"id": "form_clear",
"debug": true,
"initApi": "/api/mock2/form/initData",
"body": [
{
"type": "input-text",
"name": "name",
"label": "姓名:"
},
{
"name": "author",
"type": "hidden",
"label": "作者:"
}
]
}
]
```
### static 和 nonstatic
```schema: scope="body"
[
{
"type": "button",
"label": "静态模式",
"className": "mr-2 mb-2",
"onEvent": {
"click": {
"actions": [
{
"actionType": "static",
"componentId": "form_static"
}
]
}
}
},
{
"type": "button",
"label": "非静态模式",
"className": "mr-2 mb-2",
"onEvent": {
"click": {
"actions": [
{
"actionType": "nonstatic",
"componentId": "form_static"
}
]
}
}
},
{
"type": "form",
"id": "form_static",
"title": "表单",
"body": [
{
"type": "input-text",
"name": "text",
"label": "输入框",
"mode": "horizontal",
"value": "text"
}
]
}
]
```

View File

@ -370,6 +370,7 @@ order: 14
| clearable | `boolean` | `true` | 是否可清除 |
| embed | `boolean` | `false` | 是否内联 |
| timeConstraints | `object` | `true` | 请参考 [input-time](./input-time#控制输入范围) 里的说明 |
| isEndDate | `boolean` | `false` | 如果配置为 true会自动默认为 23:59:59 秒 |
## 事件表

View File

@ -56,7 +56,7 @@ order: 38
## 控制调整的粒度
使用 `step` 可以控制调整粒度,默认是 1。
使用 `step` 可以控制调整粒度,默认是 1。`3.3.0`版本后支持使用变量。
```schema: scope="body"
{
@ -255,14 +255,14 @@ order: 38
当做选择器表单项使用时,除了支持 [普通表单项属性表](./formitem#%E5%B1%9E%E6%80%A7%E8%A1%A8) 中的配置以外,还支持下面一些配置
| 属性名 | 类型 | 默认值 | 说明 |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| 属性名 | 类型 | 默认值 | 说明 | 版本 |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
| className | `string` | | css 类名 |
| value | `number` or `string` or `{min: number, max: number}` or `[number, number]` | | |
| min | `number` | `0` | 最小值 |
| max | `number` | `100` | 最大 |
| min | `number \| string` | `0` | 最小值,支持变量 | `3.3.0`后支持变量 |
| max | `number \| string` | `100` | 最大 支持变量值 | `3.3.0`后支持变量 |
| disabled | `boolean` | `false` | 是否禁用 |
| step | `number` | `1` | 步长 |
| step | `number \| string` | `1` | 步长,支持变量 | `3.3.0`后支持变量 |
| showSteps | `boolean` | `false` | 是否显示步长 |
| parts | `number` or `number[]` | `1` | 分割的块数<br/>主持数组传入分块的节点 |
| marks | <code>{ [number &#124; string]: ReactNode }</code> or <code>{ [number &#124; string]: { style: CSSProperties, label: ReactNode } }</code> | | 刻度标记<br/>- 支持自定义样式<br/>- 设置百分比 |

View File

@ -12,6 +12,8 @@ order: 61
> 1.10.0 及以上版本
这个组件可以基于 JSON Schema 生成表单项,方便对接类似 OpenAPI/Swagger Specification 的接口规范,可基于接口定义自动生成 amis 表单项。
> 此组件还在实验阶段,很多 json-schema 属性没有对应实现,使用前请先确认你要的功能满足了需求
基于 json-schema 定义生成表单输入项。

View File

@ -239,6 +239,61 @@ order: 58
}
```
### 导航项收纳
垂直(`"stack": true`)模式下,如果子导航项比较多,也可以给导航项设置收纳模式,配置同`overflow`,仅支持一次性展开
```schema: scope="body"
{
"type": "nav",
"stacked": true,
"expandPosition": "after",
"style": {
"width": 200
},
"links": [
{
"label": [
{
"type": "tpl",
"tpl": "Nav1"
}
],
"to": "#/"
},
{
"label": "Nav2",
"unfolded": true,
"overflow": {
"enable": true
},
"children": [
{
"label": "Nav 2-1",
"to": "#/test2"
},
{
"label": "Nav 2-2",
"to": "#/test3"
},
{
"label": "Nav 2-3",
"to": "#/test1"
},
{
"label": "Nav 2-4",
"to": "#/test4"
},
{
"label": "Nav 2-5",
"to": "#/test5"
}
]
}
]
}
```
## 动态导航
通过配置 source 来实现动态生成导航source 可以是 api 地址或者变量,比如
@ -323,13 +378,359 @@ order: 58
}
```
## 悬浮导航
可以通过设置`mode`属性来控制导航模式,不设置默认为内联导航模式
```schema: scope="body"
{
"type": "nav",
"stacked": true,
"mode": "float",
"style": {
"width": "200px"
},
"links": [
{
"label": "Nav 1",
"to": "/docs/index",
"icon": "fa fa-user",
"active": true
},
{
"label": "Nav 2",
"unfolded": true,
"children": [
{
"label": "Nav 2-1",
"children": [
{
"label": "Nav 2-1-1",
"to": "/docs/api-2-1-1"
}
]
},
{
"label": "Nav 2-2",
"to": "/docs/api-2-2"
}
]
},
{
"label": "Nav 3",
"to": "/docs/renderers"
}
]
}
```
## 导航缩起
`collapsed`属性控制导航的展开和缩起,缩起状态下,导航内容仅展示图标或第一个文字,悬浮展开全部内容或子导航项
```schema: scope="body"
{
"type": "nav",
"stacked": true,
"collapsed": true,
"links": [
{
"label": "Nav 1",
"to": "/docs/index",
"icon": "fa fa-user"
},
{
"label": "Nav 2",
"unfolded": true,
"children": [
{
"label": "Nav 2-1",
"children": [
{
"label": "Nav 2-1-1",
"to": "/docs/api-2-1-1"
}
]
},
{
"label": "Nav 2-2",
"to": "/docs/api-2-2"
}
]
},
{
"label": "Nav 3",
"to": "/docs/renderers"
}
]
}
```
## 导航分割线
导航项`mode`属性控制导航项的展示模式,支持`divider`(分割线)和`group`(分组)两种模式,不设置默认为普通导航项
```schema: scope="body"
{
"type": "nav",
"stacked": true,
"style": {
"width": "160px"
},
"links": [
{
"label": "Nav 1",
"to": "/docs/index",
"icon": "fa fa-user"
},
{
mode: 'divider'
},
{
"label": "Nav 2",
"unfolded": true,
"children": [
{
"label": "Nav 2-1",
"children": [
{
"label": "Nav 2-1-1",
"to": "/docs/api-2-1-1"
}
]
},
{
"label": "Nav 2-2",
"to": "/docs/api-2-2"
}
]
},
{
"label": "Nav 3",
"to": "/docs/renderers",
"disabled": true,
"disabledTip": "导航项禁用"
}
]
}
```
## 导航分组
分组模式(`"mode": "group"`)的导航项展示为分组标题形式
```schema: scope="body"
{
"type": "nav",
"stacked": true,
"links": [
{
"label": "Nav 1",
"to": "/docs/index",
"icon": "fa fa-user"
},
{
"label": "Nav 2",
"unfolded": true,
"mode": "group",
"children": [
{
"label": "Nav 2-1",
"children": [
{
"label": "Nav 2-1-1",
"to": "/docs/api-2-1-1"
}
]
},
{
"label": "Nav 2-2",
"to": "/docs/api-2-2"
}
]
},
{
"label": "Nav 3",
"to": "/docs/renderers",
"mode": "group",
"children": [
{
"label": "Nav 3-1",
"to": "/docs/api-2-1"
}
]
}
]
}
```
## 默认展开层级
当前导航最大层级为 4可通过`defaultOpenLevel`来控制默认 2 层级全部展开
```schema: scope="body"
{
"type": "nav",
"stacked": true,
"defaultOpenLevel": "2",
"links": [
{
"label": "Nav 1",
"to": "/docs/index",
"icon": "fa fa-user"
},
{
"label": "Nav 2",
"unfolded": true,
"children": [
{
"label": "Nav 2-1",
"children": [
{
"label": "Nav 2-1-1",
"to": "/docs/api-2-1-1",
"children": [
{
"label": "Nav 2-1-1-1",
"to": "/docs/api-2-1-1-1"
}
]
}
]
},
{
"label": "Nav 2-2",
"to": "/docs/api-2-2"
}
]
},
{
"label": "Nav 3",
"to": "/docs/renderers",
"children": [
{
"label": "Nav 3-1",
"to": "/docs/api-2-1"
}
]
}
]
}
```
## 自定义展开按钮
可以设置`expandIcon`为 icon 的名称字符串
```schema: scope="body"
{
"type": "nav",
"stacked": true,
"expandIcon": "close",
"links": [
{
"label": "Nav 1",
"to": "/docs/index",
"icon": "fa fa-user"
},
{
"label": "Nav 2",
"unfolded": true,
"children": [
{
"label": "Nav 2-1",
"children": [
{
"label": "Nav 2-1-1",
"to": "/docs/api-2-1-1"
}
]
},
{
"label": "Nav 2-2",
"to": "/docs/api-2-2"
}
]
},
{
"label": "Nav 3",
"to": "/docs/renderers",
"children": [
{
"label": "Nav 3-1",
"to": "/docs/api-2-1"
}
]
}
]
}
```
也可以将`expandIcon`设置为`SchemaObject`
```schema: scope="body"
{
"type": "nav",
"stacked": true,
"expandIcon": {
"type": "icon",
"icon": "far fa-address-book"
},
"links": [
{
"label": "Nav 1",
"to": "/docs/index",
"icon": "fa fa-user"
},
{
"label": "Nav 2",
"unfolded": true,
"children": [
{
"label": "Nav 2-1",
"children": [
{
"label": "Nav 2-1-1",
"to": "/docs/api-2-1-1"
}
]
},
{
"label": "Nav 2-2",
"to": "/docs/api-2-2"
}
]
},
{
"label": "Nav 3",
"to": "/docs/renderers",
"children": [
{
"label": "Nav 3-1",
"to": "/docs/api-2-1"
}
]
}
]
}
```
## 属性表
| 属性名 | 类型 | 默认值 | 说明 |
| --------------------------------- | ----------------------------------------- | ------------------ | ---------------------------------------------------------------------------------------- |
| type | `string` | `"nav"` | 指定为 Nav 渲染器 |
| mode | `string` | `"inline"` | 导航模式,悬浮或者内联,默认内联模式 |
| collapsed | `boolean` | | 控制导航是否缩起 |
| indentSize | `number` | `16` | 层级缩进值,仅内联模式下生效 |
| level | `number` | | 控制导航最大展示层级数 |
| defaultOpenLevel | `number` | | 控制导航最大默认展开层级 |
| className | `string` | | 外层 Dom 的类名 |
| popupClassName | `string` | | 当为悬浮模式时,可自定义悬浮层样式 |
| expandIcon | `string \| SchemaObject` | | 自定义展开按钮 |
| expandPosition | `string` | | 展开按钮位置,`"before"`或者`"after"`,不设置默认在前面 |
| stacked | `boolean` | `true` | 设置成 false 可以以 tabs 的形式展示 |
| accordion | `boolean` | | 是否开启手风琴模式 |
| source | `string` 或 [API](../../docs/types/api) | | 可以通过变量或 API 接口动态创建导航 |
| deferApi | [API](../../docs/types/api) | | 用来延时加载选项详情的接口,可以不配置,不配置公用 source 接口。 |
| itemActions | [SchemaNode](../../docs/types/schemanode) | | 更多操作相关配置 |
@ -348,6 +749,11 @@ order: 58
| links[x].activeOn | [表达式](../../docs/concepts/expression) | | 是否高亮的条件,留空将自动分析链接地址 |
| links[x].defer | `boolean` | | 标记是否为懒加载项 |
| links[x].deferApi | [API](../../docs/types/api) | | 可以不配置,如果配置优先级更高 |
| links[x].disabled | `boolean` | | 是否禁用 |
| links[x].disabledTip | `string` | | 禁用提示信息 |
| links[x].className | `string` | | 菜单项自定义样式 |
| links[x].mode | `string` | | 菜菜单项模式,分组模式:`"group"`、分割线:`"divider"` |
| links[x].overflow | `NavOverflow` | | 导航项响应式收纳配置 |
| overflow | `NavOverflow` | | 响应式收纳配置 |
| overflow.enable | `boolean` | `false` | 是否开启响应式收纳 |
| overflow.overflowLabel | `string \| SchemaObject` | | 菜单触发按钮的文字 |
@ -357,4 +763,348 @@ order: 58
| overflow.style | `React.CSSProperties` | | 自定义样式 |
| overflow.overflowClassName | `string` | `""` | 菜单按钮 CSS 类名 |
| overflow.overflowPopoverClassName | `string` | `""` | Popover 浮层 CSS 类名 |
| overflow.overflowListClassName | `string` | `""` | 菜单外层 CSS 类名 |
## 事件表
当前组件会对外派发以下事件,可以通过`onEvent`来监听这些事件,并通过`actions`来配置执行的动作,在`actions`中可以通过`${事件参数名}`或`${event.data.[事件参数名]}`来获取事件产生的数据,详细查看[事件动作](../../docs/concepts/event-action)。
| 事件名称 | 事件参数 | 说明 |
| --------- | --------------------------------------------------------------- | ------------------------ |
| loaded | `items: item[]` 数据源<br/>`item?: object` 懒加载时所点击导航项 | 异步加载数据源完成时触发 |
| collapsed | `collapsed: boolean` 缩起展开状态 | 导航缩起展开时触发 |
| toggled | `item: object` 导航项数据<br/>`open: boolean` 展开状态 | 点击导航展开按钮时触发 |
| change | `value: item[]` 选中导航项数据 | 导航项选中有变化时触发 |
| click | `item: object` 点击导航项数据 | 手动点击导航项时触发 |
### loaded
数据源加载完成,可以尝试将`source`配置为 api 地址或者开启懒加载。
```schema
{
"type": "page",
"body": {
"type": "nav",
"stacked": true,
"source": "/api/options/nav?parentId=${value}",
"onEvent": {
"loaded": {
"actions": [
{
"actionType": "toast",
"args": {
"msg": "已加载${event.data.items.length}条记录"
}
}
]
}
}
}
}
```
### collapsed
导航缩起,可以尝试修改导航的`collapsed`属性。
```schema: scope="body"
{
"type": "nav",
"stacked": true,
"collapsed": true,
"links": [
{
"label": "Nav 1",
"to": "/docs/index",
"icon": "fa fa-user"
},
{
"label": "Nav 2",
"unfolded": true,
"children": [
{
"label": "Nav 2-1",
"children": [
{
"label": "Nav 2-1-1",
"to": "/docs/api-2-1-1"
}
]
},
{
"label": "Nav 2-2",
"to": "/docs/api-2-2"
}
]
},
{
"label": "Nav 3",
"to": "/docs/renderers"
}
],
"onEvent": {
"collapsed": {
"actions": [
{
"actionType": "toast",
"args": {
"msg": "${event.data.collapsed ? '导航缩起' : '导航展开'}"
}
}
]
}
}
}
```
### toggled
导航项收起展开,可以尝试点击导航项展开按钮。
```schema: scope="body"
{
"type": "nav",
"stacked": true,
"links": [
{
"label": "Nav 1",
"to": "/docs/index",
"icon": "fa fa-user"
},
{
"label": "Nav 2",
"unfolded": true,
"children": [
{
"label": "Nav 2-1",
"children": [
{
"label": "Nav 2-1-1",
"to": "/docs/api-2-1-1"
}
]
},
{
"label": "Nav 2-2",
"to": "/docs/api-2-2"
}
]
},
{
"label": "Nav 3",
"to": "/docs/renderers"
}
],
"onEvent": {
"toggled": {
"actions": [
{
"actionType": "toast",
"args": {
"msg": "${event.data.item.label}${event.data.open ? '展开' : '收起'}"
}
}
]
}
}
}
```
### change
导航项选中,可以尝试手动修改任意导航项的`active`属性。
```schema: scope="body"
{
"type": "nav",
"stacked": true,
"links": [
{
"label": "Nav 1",
"to": "/docs/index",
"icon": "fa fa-user"
},
{
"label": "Nav 2",
"unfolded": true,
"children": [
{
"label": "Nav 2-1",
"children": [
{
"label": "Nav 2-1-1",
"to": "/docs/api-2-1-1"
}
]
},
{
"label": "Nav 2-2",
"to": "/docs/api-2-2"
}
]
},
{
"label": "Nav 3",
"to": "/docs/renderers"
}
],
"onEvent": {
"change": {
"actions": [
{
"actionType": "toast",
"args": {
"msg": "${event.data.value.length}项选中"
}
}
]
}
}
}
```
### click
导航项点击,可以尝试手动点击任意导航项。
```schema: scope="body"
{
"type": "nav",
"stacked": true,
"links": [
{
"label": "Nav 1",
"icon": "fa fa-user"
},
{
"label": "Nav 2",
"children": [
{
"label": "Nav 2-1",
"children": [
{
"label": "Nav 2-1-1"
}
]
},
{
"label": "Nav 2-2"
}
]
},
{
"label": "Nav 3"
}
],
"onEvent": {
"click": {
"actions": [
{
"actionType": "toast",
"args": {
"msg": "${event.data.item.label}被点击了,但不一定选中"
}
}
]
}
}
}
```
## 动作表
当前组件对外暴露以下特性动作,其他组件可以通过指定`actionType: 动作名称`、`componentId: 该组件id`来触发这些动作,动作配置可以通过`args: {动作配置项名称: xxx}`来配置具体的参数,详细请查看[事件动作](../../docs/concepts/event-action#触发其他组件的动作)。
| 动作名称 | 动作配置 | 说明 |
| ----------- | -------------------------------- | --------------------------------------------------------------------------------------- |
| updateItems | `value: string`或`value: item[]` | 更新导航数据源为指定导航项的子导航数据 |
| reset | | 重置导航数据源,和 updateItems 搭配使用,没有经过 updateItems 操作的,执行 reset 无效果 |
### updateItems / reset
```schema: scope="body"
{
"type": "page",
"data": {
"items": [
{
"label": "Nav 1",
"to": "/docs/index",
"icon": "fa fa-user"
},
{
"label": "Nav 2",
"unfolded": true,
"active": true,
"children": [
{
"label": "Nav 2-1",
"children": [
{
"label": "Nav 2-1-1",
"to": "/docs/api-2-1-1"
}
]
},
{
"label": "Nav 2-2",
"to": "/docs/api-2-2"
}
]
},
{
"label": "Nav 3",
"to": "/docs/renderers"
}
]
},
"body": {
"type": "container",
"body": [
{
"type": "action",
"label": "设置数据源",
"onEvent": {
"click": {
"actions": [
{
"actionType": "updateItems",
"args": {
"value": "Nav 2"
},
"componentId": "asideNav"
}
]
}
}
},
{
"type": "action",
"label": "重置数据源",
"className": "mx-1",
"onEvent": {
"click": {
"actions": [
{
"actionType": "reset",
"componentId": "asideNav"
}
]
}
}
},
{
"type": "container",
"body": [
{
"type": "nav",
"stacked": true,
"source": "${items}",
"id": "asideNav"
}
]
}
]
}
}
```

View File

@ -841,8 +841,9 @@ order: 68
| defaultKey | `string` / `number` | | 组件初始化时激活的选项卡hash 值或索引值,支持使用表达式 `2.7.1 以上版本` |
| activeKey | `string` / `number` | | 激活的选项卡hash 值或索引值,支持使用表达式,可响应上下文数据变化 |
| className | `string` | | 外层 Dom 的类名 |
| linksClassName | `string` | | Tabs 标题区的类名 |
| contentClassName | `string` | | Tabs 内容区的类名 |
| tabsMode | `string` | | 展示模式,取值可以是 `line`、`card`、`radio`、`vertical`、`chrome`、`simple`、`strong`、`tiled`、`sidebar` |
| tabsClassName | `string` | | Tabs Dom 的类名 |
| tabs | `Array` | | tabs 内容 |
| source | `string` | | tabs 关联数据,关联后可以重复生成选项卡 |
| toolbar | [SchemaNode](../types/schemanode) | | tabs 中的工具栏 |

View File

@ -256,18 +256,16 @@ run action ajax
actions: [
{
actionType: 'ajax',
args: {
api: {
url: 'https://3xsw4ap8wah59.cfc-execute.bj.baidubce.com/api/amis-mock/mock2/form/saveForm?name=${name}',
method: 'get',
"responseData": {
"resId": "${id}"
}
api: {
url: 'https://3xsw4ap8wah59.cfc-execute.bj.baidubce.com/api/amis-mock/mock2/form/saveForm?name=${name}',
method: 'post',
responseData: {
"resId": "${id}",
},
messages: {
success: '成功了!欧耶',
failed: '失败了呢。。'
}
},
},
data: {
age: 18
@ -296,18 +294,16 @@ run action ajax
actions: [
{
actionType: 'ajax',
args: {
api: {
url: 'https://3xsw4ap8wah59.cfc-execute.bj.baidubce.com/api/amis-mock/initData',
method: 'post'
},
api: {
url: 'https://3xsw4ap8wah59.cfc-execute.bj.baidubce.com/api/amis-mock/initData',
method: 'post',
messages: {
success: '成功了!欧耶',
failed: '失败了呢。。'
},
options: {
silent: true
}
},
options: {
silent: true,
},
data: {
age: 18
@ -328,9 +324,7 @@ run action ajax
}
```
**动作属性args**
> `< 1.8.0 及以下版本`,以下属性与 args 同级。
**动作属性**
| 属性名 | 类型 | 默认值 | 说明 |
| -------- | ----------------------------------- | ------ | ------------------------- |
@ -377,57 +371,55 @@ run action ajax
actions: [
{
actionType: 'dialog',
args: {
dialog: {
type: 'dialog',
title: '模态弹窗',
id: 'dialog_001',
data: {
myage: '22'
dialog: {
type: 'dialog',
title: '模态弹窗',
id: 'dialog_001',
data: {
myage: '22'
},
body: [
{
type: 'tpl',
tpl: '<p>对,你打开了模态弹窗</p>',
inline: false
},
body: [
{
type: 'tpl',
tpl: '<p>对,你打开了模态弹窗</p>',
inline: false
},
{
type: 'input-text',
name: 'myname',
mode: 'horizontal',
onEvent: {
change: {
actions: [
{
actionType: 'confirm',
componentId: 'dialog_001'
}
]
}
{
type: 'input-text',
name: 'myname',
mode: 'horizontal',
onEvent: {
change: {
actions: [
{
actionType: 'confirm',
componentId: 'dialog_001'
}
]
}
}
],
onEvent: {
confirm: {
actions: [
{
actionType: 'toast',
args: {
msg: 'confirm'
}
}
],
onEvent: {
confirm: {
actions: [
{
actionType: 'toast',
args: {
msg: 'confirm'
}
]
},
cancel: {
actions: [
{
actionType: 'toast',
args: {
msg: 'cancel'
}
}
]
},
cancel: {
actions: [
{
actionType: 'toast',
args: {
msg: 'cancel'
}
]
}
}
]
}
}
}
@ -440,9 +432,7 @@ run action ajax
}
```
**动作属性args**
> `< 2.3.2 及以下版本`,以下属性与 args 同级。
**动作属性**
| 属性名 | 类型 | 默认值 | 说明 |
| ------ | ----------------------- | ------ | --------------------------------------------------------- |
@ -466,63 +456,59 @@ run action ajax
actions: [
{
actionType: 'dialog',
args: {
dialog: {
type: 'dialog',
id: 'dialog_002',
title: '模态弹窗',
body: [
{
type: 'button',
label: '打开子弹窗,然后关闭它的父亲',
onEvent: {
click: {
actions: [
{
actionType: 'dialog',
args: {
dialog: {
type: 'dialog',
title: '模态子弹窗',
body: [
{
type: 'button',
label: '关闭指定弹窗(关闭父弹窗)',
onEvent: {
click: {
actions: [
{
actionType: 'closeDialog',
componentId: 'dialog_002'
}
]
dialog: {
type: 'dialog',
id: 'dialog_002',
title: '模态弹窗',
body: [
{
type: 'button',
label: '打开子弹窗,然后关闭它的父亲',
onEvent: {
click: {
actions: [
{
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '模态子弹窗',
body: [
{
type: 'button',
label: '关闭指定弹窗(关闭父弹窗)',
onEvent: {
click: {
actions: [
{
actionType: 'closeDialog',
componentId: 'dialog_002'
}
}
]
}
]
}
}
}
]
}
]
}
}
},
{
type: 'button',
label: '关闭当前弹窗',
className: 'ml-2',
onEvent: {
click: {
actions: [
{
actionType: 'closeDialog'
}
]
}
}
]
}
}
]
}
},
{
type: 'button',
label: '关闭当前弹窗',
className: 'ml-2',
onEvent: {
click: {
actions: [
{
actionType: 'closeDialog'
}
]
}
}
}
]
}
}
]
@ -557,38 +543,36 @@ run action ajax
actions: [
{
actionType: 'drawer',
args: {
drawer: {
type: 'drawer',
title: '模态抽屉',
body: [
{
type: 'tpl',
tpl: '<p>对,你打开了模态抽屉</p>',
inline: false
}
],
onEvent: {
confirm: {
actions: [
{
actionType: 'toast',
args: {
msg: 'confirm'
}
drawer: {
type: 'drawer',
title: '模态抽屉',
body: [
{
type: 'tpl',
tpl: '<p>对,你打开了模态抽屉</p>',
inline: false
}
],
onEvent: {
confirm: {
actions: [
{
actionType: 'toast',
args: {
msg: 'confirm'
}
]
},
cancel: {
actions: [
{
actionType: 'toast',
args: {
msg: 'cancel'
}
}
]
},
cancel: {
actions: [
{
actionType: 'toast',
args: {
msg: 'cancel'
}
]
}
}
]
}
}
}
@ -601,9 +585,7 @@ run action ajax
}
```
**动作属性args**
> `< 2.3.2 及以下版本`,以下属性与 args 同级。
**动作属性**
| 属性名 | 类型 | 默认值 | 说明 |
| ------ | ----------------------- | ------ | --------------------------------------------------------- |
@ -626,63 +608,59 @@ run action ajax
actions: [
{
actionType: 'drawer',
args: {
drawer: {
type: 'drawer',
id: 'drawer_1',
title: '模态抽屉',
body: [
{
type: 'button',
label: '打开子抽屉,然后关闭它的父亲',
onEvent: {
click: {
actions: [
{
actionType: 'drawer',
args: {
drawer: {
type: 'drawer',
title: '模态子抽屉',
body: [
{
type: 'button',
label: '关闭指定抽屉(关闭父抽屉)',
onEvent: {
click: {
actions: [
{
actionType: 'closeDrawer',
componentId: 'drawer_1'
}
]
drawer: {
type: 'drawer',
id: 'drawer_1',
title: '模态抽屉',
body: [
{
type: 'button',
label: '打开子抽屉,然后关闭它的父亲',
onEvent: {
click: {
actions: [
{
actionType: 'drawer',
drawer: {
type: 'drawer',
title: '模态子抽屉',
body: [
{
type: 'button',
label: '关闭指定抽屉(关闭父抽屉)',
onEvent: {
click: {
actions: [
{
actionType: 'closeDrawer',
componentId: 'drawer_1'
}
}
]
}
]
}
}
}
]
}
]
}
}
},
{
type: 'button',
label: '关闭当前抽屉',
className: 'ml-2',
onEvent: {
click: {
actions: [
{
actionType: 'closeDrawer'
}
]
}
}
]
}
}
]
}
},
{
type: 'button',
label: '关闭当前抽屉',
className: 'ml-2',
onEvent: {
click: {
actions: [
{
actionType: 'closeDrawer'
}
]
}
}
}
]
}
}
]
@ -699,51 +677,13 @@ run action ajax
| ----------- | -------- | ------ | --------------- |
| componentId | `string` | - | 指定抽屉组件 id |
### 打开对话框
### 打开确认弹窗
通过配置`actionType: 'alert'`或`actionType: 'confirm'`打开不同对话框,该动作分别需实现 env.alert: (msg: string) => void 和 env.confirm: (msg: string, title?: string) => boolean | Promise&lt;boolean&gt;
通过配置`actionType: 'confirmDialog'`打开确认对话框。确认对话框弹出后,如果选择取消操作,将不会执行该动作后面的动作。如下面的例子,点击确认之后将弹出`toast`提示,点击取消则不会提示
#### 提示对话框
**普通文本内容**
```schema
{
type: 'page',
data: {
msg: '去吃饭了'
},
body: [
{
type: 'button',
label: '提示对话框(模态)',
level: 'primary',
onEvent: {
click: {
actions: [
{
actionType: 'alert',
args: {
title: '提示',
msg: '<a href="http://www.baidu.com" target="_blank">${msg}~</a>'
}
}
]
}
}
}
]
}
```
**动作属性args**
> `< 1.8.0 及以下版本`,以下属性与 args 同级。
| 属性名 | 类型 | 默认值 | 说明 |
| ------ | -------- | -------- | -------------- |
| title | `string` | 系统提示 | 对话框标题 |
| msg | `string` | - | 对话框提示内容 |
#### 确认对话框
动作需要实现 env.confirm: (msg: string, title?: string) => boolean | Promise&lt;boolean&gt;
```schema
{
@ -762,10 +702,16 @@ run action ajax
actions: [
{
actionType: 'confirmDialog',
args: {
dialog: {
title: '${title}',
msg: '<span style="color:red">${msg}</span>'
}
},
{
actionType: 'toast',
args: {
msg: '确认ok啦'
}
}
]
}
@ -775,14 +721,120 @@ run action ajax
}
```
**动作属性args**
**自定义弹窗内容**
> `< 1.8.0 及以下版本`,以下属性与 args 同级
可以通过`body`像配置弹窗一样配置确认弹窗的内容
| 属性名 | 类型 | 默认值 | 说明 |
| ------ | -------- | ------ | -------------- |
| title | `string` | - | 对话框标题 |
| msg | `string` | - | 对话框提示内容 |
```schema
{
type: 'page',
data: {
title: '操作确认',
msg: '确认提交吗?'
},
body: [
{
type: 'button',
label: '自定义确认对话框(模态)',
level: 'primary',
onEvent: {
click: {
actions: [
{
actionType: 'confirmDialog',
dialog: {
type: 'dialog',
title: '${title}',
confirmText: '确认',
cancelText: '取消',
confirmBtnLevel: 'primary',
data: {
'&': '$$',
title: '确认'
},
body: [
{
"type": "form",
"initApi": "/api/mock2/form/initData",
"title": "编辑用户信息",
"body": [
{
"type": "input-text",
"name": "name",
"label": "姓名"
},
{
"type": "input-text",
"name": "email",
"label": "邮箱"
},
{
type: 'tpl',
tpl: '${msg}'
}
]
}
]
}
},
{
actionType: 'toast',
args: {
msg: '确认ok啦'
}
}
]
}
}
}
]
}
```
**动作属性**
| 属性名 | 类型 | 默认值 | 说明 |
| ------ | ----------------------------- | ------ | ------------------------------------------------------------------- |
| dialog | {msg:`string`}/`DialogObject` | - | 指定弹框内容。自定义弹窗内容可参考[Dialog](../../components/dialog) |
### 提示对话框
通过配置`actionType: 'alert'`打开提示对话框,该对话框只有确认按钮。该动作需要实现 env.alert: (msg: string) => void。
```schema
{
type: 'page',
data: {
msg: '去吃饭了'
},
body: [
{
type: 'button',
label: '提示对话框(模态)',
level: 'primary',
onEvent: {
click: {
actions: [
{
actionType: 'alert',
dialog: {
title: '提示',
msg: '<a href="http://www.baidu.com" target="_blank">${msg}~</a>'
}
}
]
}
}
}
]
}
```
**动作属性**
| 属性名 | 类型 | 默认值 | 说明 |
| ------ | -------------------------------- | ---------------------------- | ---------- |
| dialog | {title:`string`<br>msg:`string`} | {title: '系统提示', msg: ''} | 对话框配置 |
### 跳转链接
@ -1672,9 +1724,7 @@ run action ajax
}
```
**动作属性args**
> `< 2.3.2 及以下版本`,以下属性与 args 同级。
**动作属性**
| 属性名 | 类型 | 默认值 | 说明 |
| ------ | ------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
@ -1800,11 +1850,11 @@ run action ajax
有时在执行自定义 JS 的时候,希望该过程中产生的数据可以分享给后面的动作使用,此时可以通过`event.setData()`来实现事件上下文的设置,这样后面动作都可以通过事件上下文来获取共享的数据。
> 注意:直接调用`event.setData()`将修改事件的原有上下文,如果不希望覆盖可以通过`event.setData({...event.data, {xxx: xxx}})`来进行数据的合并。
> 注意:直接调用`event.setData()`将修改事件的原有上下文,如果不希望覆盖可以通过`event.setData({...event.data, ...{xxx: xxx}})`来进行数据的合并。
## 触发其他组件的动作
## 触发组件的动作
通过配置`componentId`来触发指定组件的动作,组件动作配置通过`args`传入`(> 1.9.0 及以上版本)`,动作参数请查看对应的组件的[动作表](../../components/form/index#动作表),更多示例请查看[组件事件动作示例](../../../examples/event/form)。
通过配置`componentId`或`componentName`来触发指定组件的动作(不配置将调用当前组件自己的动作),组件动作配置通过`args`传入`(> 1.9.0 及以上版本)`,动作参数请查看对应的组件的[动作表](../../components/form/index#动作表),更多示例请查看[组件事件动作示例](../../../examples/event/form)。
```schema
{

View File

@ -703,7 +703,7 @@ render 有三个参数,后面会详细说明这三个参数内的属性
(schema: any, path: string) => Promise<Function>;
```
可以通过它懒加载自定义组件,比如: https://github.com/baidu/amis/blob/master/__tests__/factory.test.tsx#L64-L91。
可以通过它懒加载自定义组件,比如: https://github.com/baidu/amis/blob/master/packages/amis-core/__tests__/factory.test.tsx#L64-L91。
#### affixOffsetTop: number

View File

@ -1,6 +1,7 @@
export default {
type: 'app',
brandName: 'APP 模式',
api: '/api/mock2/sample',
// logo:
// '<svg t="1610181550507" style="width: 30px; height: 30px;" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2528" width="200" height="200"><path d="M217.090828 534.490224c99.683327-20.926612 86.12145-137.343041 83.128279-162.838715-4.906753-39.223327-52.112891-107.812471-116.217908-102.372575-80.693834 7.058766-92.487438 120.948653-92.487438 120.948653C80.583828 442.927855 117.654119 555.449581 217.090828 534.490224L217.090828 534.490224zM322.936505 737.004567c-2.907213 8.211009-9.413394 29.170366-3.781116 47.381124 11.123338 40.874943 51.108005 34.208103 51.108005 34.208103l56.034201 0L426.297594 706.525392l-56.034201 0C345.158622 713.864544 325.65543 728.85291 322.936505 737.004567L322.936505 737.004567zM402.144498 339.233168c55.081503 0 99.57588-61.946864 99.57588-138.461515 0-76.491115-44.494377-138.40728-99.57588-138.40728-54.974056 0-99.599416 61.916165-99.599416 138.40728C302.545082 277.286304 347.170442 339.233168 402.144498 339.233168L402.144498 339.233168zM639.288546 348.395852c73.635067 9.331529 120.925117-67.407226 130.340557-125.57195 9.582239-58.081837-37.906332-125.577067-90.024339-137.174196-52.219315-11.712763-117.390617 70.044286-123.329886 123.305327C549.187459 274.094612 565.853024 339.149257 639.288546 348.395852L639.288546 348.395852zM819.642171 690.38069c0 0-113.866351-86.12145-180.37716-179.167612-90.108251-137.176243-218.121809-81.367169-260.908288-11.604292C335.745229 569.372685 269.286608 613.477182 259.841491 625.187899c-9.552563 11.489682-137.50984 79.010495-109.128443 202.314799s135.49802 131.180691 135.49802 131.180691 66.203818-3.168156 151.517879-21.829168c85.312014-18.48705 158.833495 4.598738 158.833495 4.598738s199.320605 65.22349 253.866918-60.3546C904.897903 755.497757 819.642171 690.38069 819.642171 690.38069L819.642171 690.38069zM482.333842 874.629018 342.241176 874.629018c-55.946196-1.313925-71.552639-45.587268-74.354452-51.943023-2.777253-6.473435-18.606777-36.478819-10.226922-87.473237 24.179702-76.490092 84.613096-84.695984 84.613096-84.695984l84.053348 0L426.326247 566.464449l56.036247 0 0 308.164568L482.333842 874.629018zM706.478831 874.629018l-140.090619 0c-55.16746-2.355651-56.124252-52.927443-56.124252-52.927443l0.086981-171.2155 56.037271 0L566.388213 790.57874c3.697205 15.438621 28.016077 28.015054 28.016077 28.015054l56.037271 0L650.441561 650.486074l56.038294 0L706.479855 874.629018 706.478831 874.629018zM931.037237 446.066335c0-27.816532-23.675212-111.617124-111.395066-111.617124-87.919399 0-99.660814 79.093383-99.660814 135.016043 0 53.344952 4.592598 127.816061 113.806999 125.490086C943.00071 592.633459 931.037237 474.058876 931.037237 446.066335L931.037237 446.066335zM931.037237 446.066335" p-id="2529"></path></svg>',
header: {
@ -44,6 +45,7 @@ export default {
{
label: '页面A-2',
url: '2',
schema: {
type: 'page',
title: '页面A-2',
@ -91,6 +93,8 @@ export default {
label: '列表',
url: '/crud/list',
icon: 'fa fa-list',
badge: '${count}',
badgeClassName: 'bg-info',
schemaApi: '/api/mock2/service/schema?type=crud'
}
]

View File

@ -85,9 +85,9 @@ fis.match('/mock/**', {
useCompile: false
});
fis.match('mod.js', {
useCompile: false
});
// fis.match('mod.js', {
// useCompile: false
// });
fis.match('*.scss', {
parser: fis.plugin('sass', {
@ -214,6 +214,9 @@ fis.match('{*.ts,*.jsx,*.tsx,/examples/**.js,/src/**.js,/src/**.ts}', {
isMod: true,
rExt: '.js'
});
fis.match('/examples/mod.js', {
isMod: false
});
fis.match('markdown-it/**', {
preprocessor: fis.plugin('js-require-file')
@ -492,7 +495,7 @@ if (fis.project.currentMedia() === 'publish-sdk') {
'!markdown-it/**',
'!markdown-it-html5-media/**',
'!punycode/**',
'!ooxml-viewer/**',
'!office-viewer/**',
'!fflate/**'
],
@ -534,9 +537,14 @@ if (fis.project.currentMedia() === 'publish-sdk') {
'barcode.js': ['src/components/BarCode.tsx', 'jsbarcode/**'],
'charts.js': ['zrender/**', 'echarts/**', 'echarts-stat/**', 'echarts-wordcloud/**'],
'charts.js': [
'zrender/**',
'echarts/**',
'echarts-stat/**',
'echarts-wordcloud/**'
],
'ooxml-viewer.js': ['ooxml-viewer/**', 'fflate/**'],
'office-viewer.js': ['office-viewer/**', 'fflate/**'],
'rest.js': [
'*.js',
@ -561,7 +569,7 @@ if (fis.project.currentMedia() === 'publish-sdk') {
'!uc.micro/**',
'!markdown-it/**',
'!markdown-it-html5-media/**',
'!ooxml-viewer/**',
'!office-viewer/**',
'!fflate/**'
]
}),
@ -792,7 +800,7 @@ if (fis.project.currentMedia() === 'publish-sdk') {
'!punycode/**',
'!amis-formula/**',
'!fflate/**',
'!ooxml-viewer/**',
'!office-viewer/**',
'!amis-core/**',
'!amis-ui/**',
'!amis/**'
@ -838,7 +846,12 @@ if (fis.project.currentMedia() === 'publish-sdk') {
'pkg/cropperjs.js': ['cropperjs/**', 'react-cropper/**'],
'pkg/charts.js': ['zrender/**', 'echarts/**', 'echarts-stat/**', 'echarts-wordcloud/**'],
'pkg/charts.js': [
'zrender/**',
'echarts/**',
'echarts-stat/**',
'echarts-wordcloud/**'
],
'pkg/api-mock.js': ['mock/*.ts'],
@ -853,7 +866,7 @@ if (fis.project.currentMedia() === 'publish-sdk') {
'!/examples/components/EChartsEditor/Common.tsx'
],
'pkg/ooxml-viewer.js': ['ooxml-viewer/**', 'fflate/**'],
'pkg/office-viewer.js': ['office-viewer/**', 'fflate/**'],
'pkg/rest.js': [
'**.{js,jsx,ts,tsx}',

View File

@ -5,5 +5,5 @@
"packages/amis-ui",
"packages/amis"
],
"version": "3.1.1"
"version": "3.3.0-beta.4"
}

View File

@ -56,7 +56,11 @@ function mockResponse(event, context, callback) {
function createHeaders(headers) {
let referer = '';
if (/^(https?\:\/\/[^:\/]+(?:\:\d+)?\/)/i.test(headers['Referer'])) {
if (
/^(https?\:\/\/[^:\/]+(?:\:\d+)?\/)/i.test(
headers['Referer'] || headers['referer']
)
) {
referer = RegExp.$1.replace(/\/$/, '');
}

View File

@ -94,7 +94,6 @@
"jest": {
"verbose": true,
"testEnvironment": "jsdom",
"collectCoverage": true,
"coverageReporters": [
"text",
"cobertura"
@ -119,7 +118,7 @@
"^amis\\-ui$": "<rootDir>/packages/amis-ui/src/index.tsx",
"^amis\\-core$": "<rootDir>/packages/amis-core/src/index.tsx",
"^amis\\-formula$": "<rootDir>/packages/amis-formula/src/index.ts",
"^office\\-viewer$": "<rootDir>/packages/ooxml-viewer/src/index.ts",
"^office\\-viewer$": "<rootDir>/packages/office-viewer/src/index.ts",
"^amis$": "<rootDir>/packages/amis/src/index.tsx"
},
"setupFilesAfterEnv": [

View File

@ -0,0 +1,11 @@
import {arraySlice} from '../../src/utils/arraySlice';
test(`arrayslice:test`, () => {
const testArray = [0, 1, 2, 3, 4, 5];
expect(arraySlice(testArray, ':')).toEqual([0, 1, 2, 3, 4, 5]);
expect(arraySlice(testArray, '0,3,4')).toEqual([0, 3, 4]);
expect(arraySlice(testArray, '1:2')).toEqual([1]);
expect(arraySlice(testArray, '0:-2')).toEqual([0, 1, 2, 3]);
expect(arraySlice(testArray, '1:-2')).toEqual([1, 2, 3]);
expect(arraySlice(testArray, '0,1:-2')).toEqual([0, 1, 2, 3]);
});

View File

@ -0,0 +1,26 @@
import {safeAdd, safeSub, numberFormatter} from '../../src/utils/math';
test(`math safeAdd:test`, () => {
expect(safeAdd(0.1, 0.2)).toEqual(0.3);
expect(safeAdd(0.111, 0.455)).toEqual(0.566);
expect(safeAdd(NaN, 1)).toEqual(NaN);
expect(safeAdd(NaN, NaN)).toEqual(NaN);
});
test(`math safeSub:test`, () => {
expect(safeSub(0.8, 0.1)).toEqual(0.7);
expect(safeSub(0.1, 0.111111)).toEqual(-0.011111);
expect(safeAdd(NaN, 1)).toEqual(NaN);
expect(safeAdd(NaN, NaN)).toEqual(NaN);
});
test('numberFormatter:test', () => {
expect(numberFormatter(0)).toEqual('0');
expect(numberFormatter(0, 2)).toEqual('0.00');
expect(numberFormatter(0, 8)).toEqual('0.00000000');
expect(numberFormatter(123456)).toEqual('123,456');
expect(numberFormatter(123456, 2)).toEqual('123,456.00');
expect(numberFormatter(1234567890000)).toEqual('1,234,567,890,000');
expect(numberFormatter(1234567890000, 4)).toEqual('1,234,567,890,000.0000');
expect(numberFormatter(1000000000000000)).toEqual('1,000,000,000,000,000');
});

View File

@ -1,6 +1,6 @@
{
"name": "amis-core",
"version": "3.1.5",
"version": "3.3.0-beta.4",
"description": "amis-core",
"main": "lib/index.js",
"module": "esm/index.js",
@ -45,7 +45,7 @@
"esm"
],
"dependencies": {
"amis-formula": "^3.1.1",
"amis-formula": "^3.3.0-beta.4",
"classnames": "2.3.1",
"file-saver": "^2.0.2",
"hoist-non-react-statics": "^3.3.2",
@ -69,7 +69,6 @@
},
"jest": {
"testEnvironment": "jsdom",
"collectCoverage": true,
"coverageReporters": [
"text",
"cobertura"

View File

@ -160,7 +160,7 @@ export class RootRenderer extends React.Component<RootRendererProps> {
window.open(mailto);
} else if (action.actionType === 'dialog') {
store.setCurrentAction(action);
store.openDialog(ctx, undefined, undefined, delegate);
store.openDialog(ctx, undefined, action.callback, delegate);
} else if (action.actionType === 'drawer') {
store.setCurrentAction(action);
store.openDrawer(ctx, undefined, undefined, delegate);

View File

@ -25,6 +25,20 @@ export function withRootStore<
})`;
static contextType = RootStoreContext;
static ComposedComponent = ComposedComponent as React.ComponentType<T>;
ref: any;
constructor(props: OuterProps) {
super(props);
this.refFn = this.refFn.bind(this);
}
getWrappedInstance() {
return this.ref.control;
}
refFn(ref: any) {
this.ref = ref;
}
render() {
const rootStore: IRendererStore = this.context as any;
@ -41,6 +55,7 @@ export function withRootStore<
React.ComponentProps<T>
> as any)}
{...injectedProps}
ref={this.refFn}
/>
);
}

View File

@ -24,7 +24,7 @@ export interface ListenerAction {
description?: string; // 事件描述actionType: broadcast
componentId?: string; // 组件ID用于直接执行指定组件的动作指定多个组件时使用英文逗号分隔
componentName?: string; // 组件Name用于直接执行指定组件的动作指定多个组件时使用英文逗号分隔
args?: Record<string, any>; // 动作配置,可以配置数据映射
args?: Record<string, any>; // 动作配置,可以配置数据映射。注意存在schema配置的动作都不能放在args里面避免数据域不同导致的解析错误问题
data?: Record<string, any> | null; // 动作数据参数,可以配置数据映射
dataMergeMode?: 'merge' | 'override'; // 参数模式,合并或者覆盖
outputVar?: string; // 输出数据变量名
@ -132,7 +132,7 @@ const getOmitActionProp = (type: string) => {
omitList = ['drawer'];
break;
case 'confirmDialog':
omitList = ['confirmDialog'];
omitList = ['dialog'];
break;
case 'reload':
omitList = ['resetPage'];

View File

@ -12,15 +12,13 @@ import {
export interface IAjaxAction extends ListenerAction {
action: 'ajax';
args: {
api: Api;
messages?: {
success: string;
failed: string;
};
options?: Record<string, any>;
[propName: string]: any;
api: Api;
messages?: {
success: string;
failed: string;
};
options?: Record<string, any>;
[propName: string]: any;
}
/**
@ -45,17 +43,25 @@ export class AjaxAction implements RendererAction {
throw new Error('env.fetcher is required!');
}
if (this.fetcherType === 'download' && action.actionType === 'download') {
// 兼容老的格式
if ((action as any).args?.api) {
(action as any).args.api.responseType = 'blob';
}
if ((action as any)?.api) {
(action as any).api.responseType = 'blob';
}
}
const env = event.context.env;
const silent = action?.options?.silent ?? action.args?.options?.silent;
const messages =
(action?.api as ApiObject)?.messages ??
(action.args?.api as ApiObject)?.messages;
try {
const result = await env.fetcher(
action.args?.api,
action?.api ?? action.args?.api,
action.data ?? {},
action.args?.options ?? {}
action?.options ?? action.args?.options ?? {}
);
const responseData =
!isEmpty(result.data) || result.ok
@ -75,18 +81,15 @@ export class AjaxAction implements RendererAction {
}
})
);
if (!action.args?.options?.silent) {
if (!silent) {
if (!result.ok) {
throw new ServerError(
(action.args?.api as ApiObject)?.messages?.failed ??
action.args?.messages?.failed ??
result.msg,
messages?.failed ?? action.args?.messages?.failed ?? result.msg,
result
);
} else {
const msg =
(action.args?.api as ApiObject)?.messages?.success ??
messages?.success ??
action.args?.messages?.success ??
result.msg ??
result.defaultMsg;
@ -106,7 +109,7 @@ export class AjaxAction implements RendererAction {
return result.data;
} catch (e) {
if (!action.args?.options?.silent) {
if (!silent) {
if (e.type === 'ServerError') {
const result = (e as ServerError).response;
env.notify(

View File

@ -1,4 +1,5 @@
import {RendererEvent} from '../utils/renderer-event';
import {createObject, isEmpty} from '../utils/helper';
import {
RendererAction,
ListenerAction,
@ -44,12 +45,11 @@ export class CmptAction implements RendererAction {
* id或未指定响应组件componentId使
*/
const key = action.componentId || action.componentName;
let component =
key && renderer.props.$schema[action.componentId ? 'id' : 'name'] !== key
? event.context.scoped?.[
action.componentId ? 'getComponentById' : 'getComponentByName'
](key)
: renderer;
let component = key
? event.context.scoped?.[
action.componentId ? 'getComponentById' : 'getComponentByName'
](key)
: renderer;
const dataMergeMode = action.dataMergeMode || 'merge';
@ -117,7 +117,32 @@ export class CmptAction implements RendererAction {
}
// 执行组件动作
return component?.doAction?.(action, action.args);
try {
const result = await component?.doAction?.(action, action.args, true);
if (['validate', 'submit'].includes(action.actionType)) {
event.setData(
createObject(event.data, {
[action.outputVar || `${action.actionType}Result`]: {
error: '',
payload: result?.__payload ?? component?.props?.store?.data,
responseData: result?.__response
}
})
);
}
return result;
} catch (e) {
event.setData(
createObject(event.data, {
[action.outputVar || `${action.actionType}Result`]: {
error: e.message,
errors: e.name === 'ValidateError' ? e.detail : e,
payload: component?.props?.store?.data
}
})
);
}
}
}

View File

@ -6,10 +6,13 @@ import {
ListenerContext,
registerAction
} from './Action';
import {render} from '../index';
import {createObject, filter, render} from '../index';
import {reject} from 'lodash';
export interface IAlertAction extends ListenerAction {
actionType: 'alert';
dialog?: Schema;
// 兼容历史,保留。为了和其他弹窗保持一致
args: {
msg: string;
[propName: string]: any;
@ -27,14 +30,17 @@ export interface IConfirmAction extends ListenerAction {
export interface IDialogAction extends ListenerAction {
actionType: 'dialog';
// 兼容历史保留。不建议用args
args: {
dialog: SchemaNode;
};
dialog?: SchemaNode; // 兼容历史
dialog?: SchemaNode;
}
export interface IConfirmDialogAction extends ListenerAction {
actionType: 'confirmDialog';
dialog?: Schema;
// 兼容历史保留。不建议用args
args: {
msg: string;
title: string;
@ -70,7 +76,7 @@ export class DialogAction implements RendererAction {
event,
{
actionType: 'dialog',
dialog: action.args?.dialog || action.dialog,
dialog: action.dialog ?? action.args?.dialog,
reload: 'none'
},
action.data
@ -121,7 +127,10 @@ export class AlertAction implements RendererAction {
renderer: ListenerContext,
event: RendererEvent<any>
) {
event.context.env.alert?.(action.args?.msg, action.args?.title);
event.context.env.alert?.(
filter(action.dialog?.msg, event.data) ?? action.args?.msg,
filter(action.dialog?.title, event.data) ?? action.args?.title
);
}
}
@ -134,22 +143,49 @@ export class ConfirmAction implements RendererAction {
renderer: ListenerContext,
event: RendererEvent<any>
) {
let content = action.args?.body
? render(action.args.body)
: action.args.msg;
const type = action.dialog?.type ?? (action.args as any)?.type;
if (!type) {
const confirmed = await event.context.env.confirm?.(
filter(action.dialog?.msg, event.data) || action.args?.msg,
filter(action.dialog?.title, event.data) || action.args?.title,
{
closeOnEsc:
filter(action.dialog?.closeOnEsc, event.data) ||
action.args?.closeOnEsc,
size: filter(action.dialog?.size, event.data) || action.args?.size,
confirmText:
filter(action.dialog?.confirmText, event.data) ||
action.args?.confirmText,
cancelText:
filter(action.dialog?.cancelText, event.data) ||
action.args?.cancelText,
confirmBtnLevel:
filter(action.dialog?.confirmBtnLevel, event.data) ||
action.args?.confirmBtnLevel,
cancelBtnLevel:
filter(action.dialog?.cancelBtnLevel, event.data) ||
action.args?.cancelBtnLevel
}
);
return confirmed;
}
// 自定义弹窗内容
const confirmed = await new Promise((resolve, reject) => {
renderer.props.onAction?.(
event,
{
actionType: 'dialog',
dialog: action.dialog ?? action.args,
reload: 'none',
callback: (result: boolean) => resolve(result)
},
action.data
);
});
const confirmed = await event.context.env.confirm?.(
content,
action.args.title,
{
closeOnEsc: action.args.closeOnEsc,
size: action.args.size,
confirmText: action.args.confirmText,
cancelText: action.args.cancelText,
confirmBtnLevel: action.args.confirmBtnLevel,
cancelBtnLevel: action.args.cancelBtnLevel
}
);
return confirmed;
}
}

View File

@ -9,10 +9,11 @@ import {
export interface IDrawerAction extends ListenerAction {
actionType: 'drawer';
// 兼容历史保留。不建议用args
args: {
drawer: SchemaNode;
};
drawer?: SchemaNode; // 兼容历史
drawer?: SchemaNode;
}
/**
@ -32,7 +33,7 @@ export class DrawerAction implements RendererAction {
event,
{
actionType: 'drawer',
drawer: action.args?.drawer || action.drawer,
drawer: action.drawer ?? action.args?.drawer,
reload: 'none'
},
action.data

View File

@ -49,6 +49,7 @@ import LazyComponent from '../components/LazyComponent';
import {isAlive} from 'mobx-state-tree';
import type {LabelAlign} from './Item';
import {injectObjectChain} from '../utils';
export interface FormHorizontal {
left?: number;
@ -661,7 +662,7 @@ export default class Form extends React.Component<FormProps, object> {
const {data, store, dispatchEvent} = this.props;
if (store.fetching) {
return;
return value;
}
// 派发init事件参数为初始化数据
@ -810,7 +811,8 @@ export default class Form extends React.Component<FormProps, object> {
const {interval, silentPolling, stopAutoRefreshWhen, data} = this.props;
clearTimeout(this.timer);
interval &&
value?.ok &&
interval &&
this.mounted &&
(!stopAutoRefreshWhen || !evalExpression(stopAutoRefreshWhen, data)) &&
(this.timer = setTimeout(
@ -824,12 +826,20 @@ export default class Form extends React.Component<FormProps, object> {
return this.props.store.validated;
}
validate(forceValidate?: boolean): Promise<boolean> {
const {store, dispatchEvent, data} = this.props;
validate(
forceValidate?: boolean,
throwErrors: boolean = false
): Promise<boolean> {
const {store, dispatchEvent, data, messages, translate: __} = this.props;
this.flush();
return store
.validate(this.hooks['validate'] || [], forceValidate)
.validate(
this.hooks['validate'] || [],
forceValidate,
throwErrors,
__(messages && messages.validateFailed)
)
.then((result: boolean) => {
if (result) {
dispatchEvent('validateSucc', data);
@ -863,7 +873,10 @@ export default class Form extends React.Component<FormProps, object> {
store.setValues(value, undefined, replace);
}
submit(fn?: (values: object) => Promise<any>): Promise<any> {
submit(
fn?: (values: object) => Promise<any>,
throwErrors: boolean = false
): Promise<any> {
const {store, messages, translate: __, dispatchEvent, data} = this.props;
this.flush();
const validateErrCb = () => dispatchEvent('validateError', data);
@ -871,7 +884,8 @@ export default class Form extends React.Component<FormProps, object> {
fn,
this.hooks['validate'] || [],
__(messages && messages.validateFailed),
validateErrCb
validateErrCb,
throwErrors
);
}
@ -1132,7 +1146,7 @@ export default class Form extends React.Component<FormProps, object> {
action.target &&
this.reloadTarget(filterTarget(action.target, values), values);
} else if (action.actionType === 'dialog') {
store.openDialog(data);
store.openDialog(data, undefined, action.callback);
} else if (action.actionType === 'drawer') {
store.openDrawer(data);
} else if (isEffectiveApi(action.api || api, values)) {
@ -1201,7 +1215,10 @@ export default class Form extends React.Component<FormProps, object> {
}
}
// return values;
return injectObjectChain(store.data, {
__payload: values,
__response: response
});
});
} else {
// type为submit但是没有配api以及target时只派发事件
@ -1209,7 +1226,7 @@ export default class Form extends React.Component<FormProps, object> {
}
return Promise.resolve(null);
})
}, throwErrors)
.then(values => {
// 有可能 onSubmit return false 了,那么后面的就不应该再执行了。
if (values === false) {
@ -1255,10 +1272,10 @@ export default class Form extends React.Component<FormProps, object> {
store.clear(onReset);
} else if (action.actionType === 'validate') {
store.setCurrentAction(action);
this.validate(true);
return this.validate(true, throwErrors);
} else if (action.actionType === 'dialog') {
store.setCurrentAction(action);
store.openDialog(data);
store.openDialog(data, undefined, action.callback);
} else if (action.actionType === 'drawer') {
store.setCurrentAction(action);
store.openDrawer(data);
@ -1326,9 +1343,28 @@ export default class Form extends React.Component<FormProps, object> {
handleQuery(query: any) {
if (this.props.initApi) {
// 如果是分页动作,则看接口里面有没有用,没用则 return false
// 让组件自己去排序
if (
query?.hasOwnProperty('orderBy') &&
!isApiOutdated(
this.props.initApi,
this.props.initApi,
this.props.store.data,
createObject(this.props.store.data, query)
)
) {
return false;
}
this.receive(query);
return;
}
if (this.props.onQuery) {
return this.props.onQuery(query);
} else {
this.props.onQuery?.(query);
return false;
}
}

View File

@ -1015,11 +1015,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
tooltip: labelRemark,
useMobileUI,
className: cx(`Form-labelRemark`),
container: props.popOverContainer
? props.popOverContainer
: env && env.getModalContainer
? env.getModalContainer
: undefined
container: props.popOverContainer || env.getModalContainer
})
: null}
</span>
@ -1048,11 +1044,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
tooltip: remark,
className: cx(`Form-remark`),
useMobileUI,
container: props.popOverContainer
? props.popOverContainer
: env && env.getModalContainer
? env.getModalContainer
: undefined
container: props.popOverContainer || env.getModalContainer
})
: null}
@ -1146,11 +1138,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
tooltip: labelRemark,
className: cx(`Form-lableRemark`),
useMobileUI,
container: props.popOverContainer
? props.popOverContainer
: env && env.getModalContainer
? env.getModalContainer
: undefined
container: props.popOverContainer || env.getModalContainer
})
: null}
</span>
@ -1174,10 +1162,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
className: cx(`Form-remark`),
tooltip: remark,
useMobileUI,
container:
env && env.getModalContainer
? env.getModalContainer
: undefined
container: props.popOverContainer || env.getModalContainer
})
: null}
@ -1221,10 +1206,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
className: cx(`Form-remark`),
tooltip: remark,
useMobileUI,
container:
env && env.getModalContainer
? env.getModalContainer
: undefined
container: props.popOverContainer || env.getModalContainer
})
: null}
@ -1322,11 +1304,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
tooltip: labelRemark,
className: cx(`Form-lableRemark`),
useMobileUI,
container: props.popOverContainer
? props.popOverContainer
: env && env.getModalContainer
? env.getModalContainer
: undefined
container: props.popOverContainer || env.getModalContainer
})
: null}
</span>
@ -1349,11 +1327,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
className: cx(`Form-remark`),
tooltip: remark,
useMobileUI,
container: props.popOverContainer
? props.popOverContainer
: env && env.getModalContainer
? env.getModalContainer
: undefined
container: props.popOverContainer || env.getModalContainer
})
: null}
@ -1449,11 +1423,8 @@ export class FormItemWrap extends React.Component<FormItemProps> {
tooltip: labelRemark,
className: cx(`Form-lableRemark`),
useMobileUI,
container: props.popOverContainer
? props.popOverContainer
: env && env.getModalContainer
? env.getModalContainer
: undefined
container:
props.popOverContainer || env.getModalContainer
})
: null}
</span>
@ -1474,10 +1445,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
icon: remark.icon || 'warning-mark',
className: cx(`Form-remark`),
tooltip: remark,
container:
env && env.getModalContainer
? env.getModalContainer
: undefined
container: props.popOverContainer || env.getModalContainer
})
: null}
</div>

View File

@ -423,6 +423,7 @@ export function wrapControl<
? 'formInited'
: 'dataChanged'
);
this.checkValidate();
}
}
}
@ -513,32 +514,44 @@ export function wrapControl<
}
}
checkValidate() {
if (!this.model) return; // 如果 model 为 undefined 则直接返回
const validated = this.model.validated;
const {formSubmited, validateOnChange} = this.props;
if (
// 如果配置了 minLength 或者 maxLength 就切成及时验证
// this.model.rules.minLength ||
// this.model.rules.maxLength ||
validateOnChange === true ||
(validateOnChange !== false && (formSubmited || validated))
) {
this.validate();
} else if (validateOnChange === false) {
this.model?.reset();
}
}
async validate() {
if (!this.model) return;
const {formStore: form, data, formItemDispatchEvent} = this.props;
let result;
if (this.model) {
if (
this.model.unique &&
form?.parentStore &&
form.parentStore.storeType === ComboStore.name
) {
const combo = form.parentStore as IComboStore;
const group = combo.uniques.get(
this.model.name
) as IUniqueGroup;
const validPromises = group.items.map(item =>
item.validate(data)
);
result = await Promise.all(validPromises);
} else {
const validPromises = form
?.getItemsByName(this.model.name)
.map(item => item.validate(data));
if (validPromises && validPromises.length) {
result = await Promise.all(validPromises);
}
}
if (
this.model.unique &&
form?.parentStore &&
form.parentStore.storeType === ComboStore.name
) {
const combo = form.parentStore as IComboStore;
const group = combo.uniques.get(this.model.name) as IUniqueGroup;
const validPromises = group.items.map(item =>
item.validate(data)
);
result = await Promise.all(validPromises);
} else {
result = [await this.model.validate(data)];
}
if (result && result.length) {
if (result.indexOf(false) > -1) {
formItemDispatchEvent('formItemValidateError', data);
@ -655,20 +668,8 @@ export function wrapControl<
return;
}
const validated = this.model.validated;
onChange?.(value, name!, submitOnChange === true);
if (
// 如果配置了 minLength 或者 maxLength 就切成及时验证
// this.model.rules.minLength ||
// this.model.rules.maxLength ||
validateOnChange === true ||
(validateOnChange !== false && (formSubmited || validated))
) {
this.validate();
} else if (validateOnChange === false) {
this.model?.reset();
}
this.checkValidate();
}
handleBlur(e: any) {

View File

@ -9,6 +9,7 @@ import {
mapTree
} from '../utils/helper';
import {ServiceStore} from './service';
import {filter, isVisible, resolveVariableAndFilter} from '../utils';
export const AppStore = ServiceStore.named('AppStore')
.props({
@ -21,7 +22,7 @@ export const AppStore = ServiceStore.named('AppStore')
get navigations(): Array<NavigationObject> {
if (Array.isArray(self.pages)) {
return mapTree(self.pages, item => {
let visible = item.visible;
let visible = isVisible(item, self.data);
if (
visible !== false &&
@ -38,7 +39,12 @@ export const AppStore = ServiceStore.named('AppStore')
path: item.path,
children: item.children,
className: item.className,
visible
visible,
badge:
typeof item.badge === 'string'
? filter(item.badge, self.data)
: item.badge,
badgeClassName: filter(item.badgeClassName, self.data)
};
});
}

View File

@ -147,9 +147,15 @@ export const ComboStore = iRendererStore
});
self.forms.forEach(form =>
form.items.forEach(
item => item.unique && item.syncOptions(undefined, form.data)
)
form.items.forEach(item => {
if (item.unique) {
item.syncOptions(undefined, form.data);
if (item.errors.length) {
item.validate(item.tmpValue);
}
}
})
);
}
}

View File

@ -1,5 +1,4 @@
import {saveAs} from 'file-saver';
import {filter} from 'amis-core';
import {types, flow, getEnv, isAlive, Instance} from 'mobx-state-tree';
import {IRendererStore} from './index';
import {ServiceStore} from './service';
@ -17,6 +16,7 @@ import pick from 'lodash/pick';
import {resolveVariableAndFilter} from '../utils/tpl-builtin';
import {normalizeApiResponseData} from '../utils/api';
import {matchSorter} from 'match-sorter';
import {filter} from '../utils/tpl';
class ServerError extends Error {
type = 'ServerError';
@ -289,14 +289,16 @@ export const CRUDStore = ServiceStore.named('CRUDStore')
items = result.items || result.rows;
}
// 如果不按照 items 格式返回,就拿第一个数组当成 items
if (!Array.isArray(items)) {
// 如果不按照 items 格式返回,就拿第一个数组当成 items
for (const key of Object.keys(result)) {
if (result.hasOwnProperty(key) && Array.isArray(result[key])) {
items = result[key];
break;
}
}
} else if (items == null) {
items = [];
}
if (!Array.isArray(items)) {

View File

@ -17,7 +17,8 @@ import {
isEmpty,
mapObject,
keyToPath,
isObject
isObject,
ValidateError
} from '../utils/helper';
import isEqual from 'lodash/isEqual';
import flatten from 'lodash/flatten';
@ -92,7 +93,7 @@ export const FormStore = ServiceStore.named('FormStore')
/** 获取InputGroup的子元素 */
get inputGroupItems() {
const formItems: Record<string, IFormItemStore[]> = {};
const children = self.children.concat();
const children: Array<any> = this.directItems.concat();
while (children.length) {
const current = children.shift();
@ -526,37 +527,20 @@ export const FormStore = ServiceStore.named('FormStore')
fn?: (values: object) => Promise<any>,
hooks?: Array<() => Promise<any>>,
failedMessage?: string,
validateErrCb?: () => void
validateErrCb?: () => void,
throwErrors?: boolean
) => Promise<any> = flow(function* submit(
fn: any,
hooks?: Array<() => Promise<any>>,
failedMessage?: string,
validateErrCb?: () => void
validateErrCb?: () => void,
throwErrors?: boolean
) {
self.submited = true;
self.submiting = true;
try {
let valid = yield validate(hooks);
// 如果不是valid而且有包含不是remote的报错的表单项时不可提交
if (
(!valid &&
self.items.some(item =>
item.errorData.some(e => e.tag !== 'remote')
)) ||
self.restError.length
) {
let msg = failedMessage ?? self.__('Form.validateFailed');
let dispatcher: any = validateErrCb && validateErrCb();
if (dispatcher?.then) {
dispatcher = yield dispatcher;
}
if (!dispatcher?.prevented) {
msg && toastValidateError(msg);
}
throw new Error(msg);
}
yield validate(hooks, undefined, true, failedMessage, validateErrCb);
if (fn) {
const diff = difference(self.data, self.pristine);
@ -581,10 +565,16 @@ export const FormStore = ServiceStore.named('FormStore')
const validate: (
hooks?: Array<() => Promise<any>>,
forceValidate?: boolean
forceValidate?: boolean,
throwErrors?: boolean,
failedMessage?: string,
validateErrCb?: () => void
) => Promise<boolean> = flow(function* validate(
hooks?: Array<() => Promise<any>>,
forceValidate?: boolean
forceValidate?: boolean,
throwErrors?: boolean,
failedMessage?: string,
validateErrCb?: () => void
) {
self.validated = true;
const items = self.directItems.concat();
@ -632,6 +622,32 @@ export const FormStore = ServiceStore.named('FormStore')
}
}
if (!self.valid) {
// 如果不是valid而且有包含不是remote的报错的表单项时不可提交
if (
self.items.some(item =>
item.errorData.some(e => e.tag !== 'remote')
) ||
self.restError.length
) {
let msg = failedMessage ?? self.__('Form.validateFailed');
let dispatcher: any = validateErrCb && validateErrCb();
if (dispatcher?.then) {
dispatcher = yield dispatcher;
}
if (!dispatcher?.prevented) {
msg && toastValidateError(msg);
}
}
if (throwErrors) {
throw new ValidateError(
failedMessage || self.__('Form.validateFailed'),
self.errors
);
}
}
return self.valid;
});

View File

@ -100,6 +100,7 @@ export const ListStore = iRendererStore
draggable: false,
dragging: false,
multiple: true,
strictMode: false,
selectable: false,
itemCheckableOn: '',
itemDraggableOn: '',
@ -167,6 +168,7 @@ export const ListStore = iRendererStore
config.selectable === void 0 || (self.selectable = config.selectable);
config.draggable === void 0 || (self.draggable = config.draggable);
config.multiple === void 0 || (self.multiple = config.multiple);
config.strictMode === void 0 || (self.strictMode = config.strictMode);
config.hideCheckToggler === void 0 ||
(self.hideCheckToggler = config.hideCheckToggler);
@ -212,11 +214,13 @@ export const ListStore = iRendererStore
if (~selected.indexOf(item.pristine)) {
self.selectedItems.push(item);
} else if (
find(
selected,
a =>
a[valueField || 'value'] == item.pristine[valueField || 'value']
)
find(selected, a => {
const selectValue = a[valueField || 'value'];
const itemValue = item.pristine[valueField || 'value'];
return self.strictMode
? selectValue === itemValue
: selectValue == itemValue;
})
) {
self.selectedItems.push(item);
}

View File

@ -27,7 +27,8 @@ import {
difference,
immutableExtends,
extendObject,
hasVisibleExpression
hasVisibleExpression,
sortArray
} from '../utils/helper';
import {evalExpression} from '../utils/tpl';
import {IFormStore} from './form';
@ -1353,6 +1354,19 @@ export const TableStore = iRendererStore
self.orderDir = key ? direction : '';
}
function changeOrder(key: string, direction: 'asc' | 'desc' | '') {
setOrderByInfo(key, direction);
const dir = /desc/i.test(self.orderDir) ? -1 : 1;
self.rows.replace(
sortArray(
self.rows.concat(),
self.orderBy,
dir,
(item, field) => item.data[field]
)
);
}
function reset() {
self.rows.forEach(item => item.reset());
let rows = self.rows.concat();
@ -1490,6 +1504,7 @@ export const TableStore = iRendererStore
collapseAllAtDepth,
clear,
setOrderByInfo,
changeOrder,
reset,
toggleDragging,
stopDragging,

View File

@ -1,4 +1,3 @@
import {attachmentAdpator} from 'amis-core';
import omit from 'lodash/omit';
import {Api, ApiObject, EventTrack, fetcherResult, Payload} from '../types';
import {fetcherConfig} from '../factory';
@ -696,11 +695,20 @@ export function isApiOutdated(
}
nextApi = normalizeApi(nextApi);
prevApi = (prevApi ? normalizeApi(prevApi) : prevApi) as ApiObject;
if (nextApi.autoRefresh === false) {
return false;
}
// api 本身有变化
if ((prevApi && prevApi.url !== nextApi.url) || !prevApi) {
return !!(
isValidApi(nextApi.url) &&
(!nextApi.sendOn || evalExpression(nextApi.sendOn, nextData))
);
}
const trackExpression = nextApi.trackExpression ?? nextApi.url;
if (typeof trackExpression !== 'string' || !~trackExpression.indexOf('$')) {
return false;
@ -708,24 +716,18 @@ export function isApiOutdated(
let isModified = false;
if (prevApi) {
prevApi = normalizeApi(prevApi);
if (nextApi.trackExpression || prevApi.trackExpression) {
isModified =
tokenize(prevApi.trackExpression || '', prevData) !==
tokenize(nextApi.trackExpression || '', nextData);
} else {
prevApi = buildApi(prevApi as Api, prevData as object, {
ignoreData: true
});
nextApi = buildApi(nextApi as Api, nextData as object, {
ignoreData: true
});
isModified = prevApi.url !== nextApi.url;
}
if (nextApi.trackExpression || prevApi.trackExpression) {
isModified =
tokenize(prevApi.trackExpression || '', prevData) !==
tokenize(nextApi.trackExpression || '', nextData);
} else {
isModified = true;
prevApi = buildApi(prevApi as Api, prevData as object, {
ignoreData: true
});
nextApi = buildApi(nextApi as Api, nextData as object, {
ignoreData: true
});
isModified = prevApi.url !== nextApi.url;
}
return !!(
@ -736,10 +738,25 @@ export function isApiOutdated(
}
export function isValidApi(api: string) {
return (
api &&
/^(?:(https?|wss?|taf):\/\/[^\/]+)?(\/?[^\s\/\?]*){1,}(\?.*)?$/.test(api)
);
if (!api || typeof api !== 'string') {
return false;
}
const idx = api.indexOf('://');
// 不允许直接相对路径写 api
// 不允许 :// 结尾
if ((!~idx && api[0] !== '/') || (~idx && idx + 3 === api.length)) {
return false;
}
try {
// 不补一个协议URL 判断为 false
api = (~idx ? '' : 'schema://domain') + api;
new URL(api);
} catch (error) {
return false;
}
return true;
}
export function isEffectiveApi(

View File

@ -0,0 +1,67 @@
/**
* 使 python
*
* * 1,2,3
* * 3:10
* * 1,2,3:10
* * :-1
*/
import {toJS, isObservableArray} from 'mobx';
export function arraySlice(array: any[], slice: string) {
if (typeof slice !== 'string') {
return array;
}
if (isObservableArray(array)) {
array = toJS(array);
}
slice = slice.trim();
if (!slice || !Array.isArray(array)) {
return array;
}
const parts = slice.split(',');
const ret: any[] = [];
const arrayLength = array.length;
if (!arrayLength) {
return array;
}
for (const part of parts) {
// 普通的场景
if (part.indexOf(':') === -1) {
const index = parseInt(part, 10);
if (!isNaN(index) && index < arrayLength) {
ret.push(array[index]);
}
} else {
const [start, end] = part.split(':');
let startIndex = parseInt(start || '0', 10);
if (isNaN(startIndex) || startIndex < 0) {
startIndex = 0;
}
// 大于就没意义了
if (startIndex >= arrayLength) {
continue;
}
let endIndex = parseInt(end, 10);
if (isNaN(endIndex)) {
endIndex = arrayLength;
}
// 负数就从后面开始取
if (endIndex < 0) {
endIndex = arrayLength + endIndex;
}
// 小于没有意义
if (endIndex < startIndex) {
continue;
}
if (endIndex > arrayLength) {
endIndex = arrayLength;
}
ret.push(...array.slice(startIndex, endIndex));
}
}
return ret;
}

View File

@ -99,19 +99,21 @@ export function dataMapping(
);
}
} else {
Object.keys(to).forEach(key => {
const value = to[key];
let keys: Array<string>;
const objectKeys = Object.keys(to);
// 如果存在 '&' 作为 key则特殊处理
// 就是无论这个 key 的位置在什么地方始终都是当放在最前面
const idx = objectKeys.indexOf('&');
if (~idx) {
const value = to['&'];
objectKeys.splice(idx, 1);
if (typeof ignoreFunction === 'function' && ignoreFunction(key, value)) {
// 如果被ignore不做数据映射处理。
setVariable(ret, key, value, convertKeyToPath);
} else if (key === '&' && value === '$$') {
if (value === '$$') {
ret = {
...ret,
...from
};
} else if (key === '&') {
} else {
let keys: Array<string>;
const v =
isPlainObject(value) &&
(keys = Object.keys(value)) &&
@ -142,9 +144,22 @@ export function dataMapping(
...v
};
}
}
}
objectKeys.forEach(key => {
const value = to[key];
if (typeof ignoreFunction === 'function' && ignoreFunction(key, value)) {
// 如果被ignore不做数据映射处理。
setVariable(ret, key, value, convertKeyToPath);
} else if (value === '$$') {
setVariable(ret, key, from, convertKeyToPath);
} else if (value && value[0] === '$') {
} else if (
typeof value === 'string' &&
value.length > 0 &&
value[0] === '$'
) {
const v = resolveMapping(value, from, undefined, ignoreIfNotMatch);
setVariable(ret, key, v, convertKeyToPath);

View File

@ -279,6 +279,10 @@ export function getStyleNumber(element: HTMLElement, styleName: string) {
/** 根据关键字高亮显示文本内容 */
export function renderTextByKeyword(rendererText: string, curKeyword: string) {
if (!rendererText || typeof rendererText !== 'string') {
return rendererText;
}
if (curKeyword && ~rendererText.indexOf(curKeyword)) {
const keywordStartIndex = rendererText.indexOf(curKeyword);
const keywordEndIndex = keywordStartIndex + curKeyword.length;

View File

@ -1273,12 +1273,13 @@ export const bulkBindFunctions = function <
export function sortArray<T extends any>(
items: Array<T>,
field: string,
dir: -1 | 1
dir: -1 | 1,
fieldGetter?: (item: T, field: string) => any
): Array<T> {
return items.sort((a: any, b: any) => {
let ret: number;
const a1 = a[field];
const b1 = b[field];
const a1 = fieldGetter ? fieldGetter(a, field) : a[field];
const b1 = fieldGetter ? fieldGetter(b, field) : b[field];
if (typeof a1 === 'number' && typeof b1 === 'number') {
ret = a1 < b1 ? -1 : a1 === b1 ? 0 : 1;
@ -1310,11 +1311,12 @@ export function qsstringify(
},
keepEmptyArray?: boolean
) {
// qs会保留空字符串。fix: Combo模式的空数组无法清空。改为存为空字符串只转换一层
keepEmptyArray &&
Object.keys(data).forEach((key: any) => {
Array.isArray(data[key]) && !data[key].length && (data[key] = '');
});
// qs会保留空字符串。fix: Combo模式的空数组无法清空。改为存为空字符串
if (keepEmptyArray) {
data = JSONValueMap(data, value =>
Array.isArray(value) && !value.length ? '' : value
);
}
return qs.stringify(data, options);
}
@ -1406,7 +1408,7 @@ export function chainEvents(props: any, schema: any) {
ret[key] = chainFunctions(schema[key], props[key]);
}
} else {
ret[key] = props[key];
ret[key] = props[key] ?? schema[key];
}
});
@ -1487,6 +1489,21 @@ export function loadStyle(href: string) {
export class SkipOperation extends Error {}
export class ValidateError extends Error {
name: 'ValidateError';
detail: {[propName: string]: Array<string> | string};
constructor(
message: string,
error: {[propName: string]: Array<string> | string}
) {
super();
this.name = 'ValidateError';
this.message = message;
this.detail = error;
}
}
/**
* https://stackoverflow.com/a/34909127
* @param obj
@ -1586,8 +1603,13 @@ export function getPropValue<
name?: string;
data?: any;
defaultValue?: any;
canAccessSuperData?: boolean;
}
>(props: T, getter?: (props: T) => any, canAccessSuper?: boolean) {
>(
props: T,
getter?: (props: T) => any,
canAccessSuper = props.canAccessSuperData
) {
const {name, value, data, defaultValue} = props;
return (
value ??
@ -1741,6 +1763,73 @@ export function JSONTraverse(
});
}
/**
*
* @param json
* @param mapper
* @returns
*/
export function JSONValueMap(
json: any,
mapper: (
value: any,
key: string | number,
host: Object,
stack: Array<Object>
) => any,
stack: Array<Object> = []
) {
if (!isPlainObject(json) && !Array.isArray(json)) {
return json;
}
const iterator = (
origin: any,
key: number | string,
host: any,
stack: Array<any> = []
) => {
let maped: any = mapper(origin, key, host, stack);
if (maped === origin && (isPlainObject(origin) || Array.isArray(origin))) {
return JSONValueMap(origin, mapper, stack);
}
return maped;
};
if (Array.isArray(json)) {
let flag = false;
let mapped = json.map((value, index) => {
let result: any = iterator(value, index, json, [json].concat(stack));
if (result !== value) {
flag = true;
return result;
}
return value;
});
return flag ? mapped : json;
}
let flag = false;
const toUpdate: any = {};
Object.keys(json).forEach(key => {
const value: any = json[key];
let result: any = iterator(value, key, json, [json].concat(stack));
if (result !== value) {
flag = true;
toUpdate[key] = result;
return;
}
});
return flag
? {
...json,
...toUpdate
}
: json;
}
export function convertArrayValueToMoment(
value: number[],
types: string[],

View File

@ -54,6 +54,8 @@ export * from './toNumber';
export * from './decodeEntity';
export * from './style-helper';
export * from './resolveCondition';
export * from './arraySlice';
export * from './math';
import animation from './Animation';

View File

@ -0,0 +1,42 @@
export function safeAdd(arg1: number, arg2: number) {
let digits1, digits2, maxDigits;
try {
digits1 = arg1.toString().split('.')[1].length;
} catch (e) {
digits1 = 0;
}
try {
digits2 = arg2.toString().split('.')[1].length;
} catch (e) {
digits2 = 0;
}
maxDigits = Math.pow(10, Math.max(digits1, digits2));
return (arg1 * maxDigits + arg2 * maxDigits) / maxDigits;
}
//减
export function safeSub(arg1: number, arg2: number) {
let digits1, digits2, maxDigits;
try {
digits1 = arg1.toString().split('.')[1].length;
} catch (e) {
digits1 = 0;
}
try {
digits2 = arg2.toString().split('.')[1].length;
} catch (e) {
digits2 = 0;
}
maxDigits = Math.pow(10, Math.max(digits1, digits2));
return (arg1 * maxDigits - arg2 * maxDigits) / maxDigits;
}
export function numberFormatter(num: number | string, precision: number = 0) {
const ZERO = 0;
const number = +num;
if (typeof number === 'number' && !isNaN(number)) {
const regexp = precision ? /(\d)(?=(\d{3})+\.)/g : /(\d)(?=(\d{3})+$)/g;
return number.toFixed(precision).replace(regexp, '$1,');
}
return ZERO.toFixed(precision);
}

View File

@ -9,13 +9,21 @@ export const normalizeLink = (to: string, location = window.location) => {
const idx = to.indexOf('?');
const idx2 = to.indexOf('#');
let pathname = ~idx
? to.substring(0, idx)
: ~idx2
? to.substring(0, idx2)
: to;
let search = ~idx ? to.substring(idx, ~idx2 ? idx2 : undefined) : '';
let hash = ~idx2 ? to.substring(idx2) : location.hash;
let pathname = to;
let search = '';
let hash = location.hash;
// host?a=a#b 的情况
if (idx < idx2) {
pathname = ~idx ? to.substring(0, idx) : ~idx2 ? to.substring(0, idx2) : to;
hash = ~idx2 ? to.substring(idx2) : location.hash;
search = ~idx ? to.substring(idx, ~idx2 ? idx2 : undefined) : '';
}
// host#b?a=a 的情况
else if (idx > idx2) {
pathname = ~idx2 ? to.substring(0, idx2) : ~idx ? to.substring(0, idx) : to;
hash = ~idx2 ? to.substring(idx2, ~idx ? idx : undefined) : location.hash;
search = ~idx ? to.substring(idx) : '';
}
if (!pathname) {
pathname = location.pathname;
@ -33,5 +41,6 @@ export const normalizeLink = (to: string, location = window.location) => {
pathname = paths.concat(pathname).join('/');
}
return pathname + search + hash;
const rest = idx < idx2 ? search + hash : hash + search;
return pathname + rest;
};

View File

@ -61,6 +61,18 @@ export function createObjectFromChain(chain: Array<object>) {
});
}
/**
*
* @param obj
* @param value
* @returns
*/
export function injectObjectChain(obj: any, value: any) {
const chain = extractObjectChain(obj);
chain.splice(chain.length - 1, 0, value);
return createObjectFromChain(chain);
}
export function cloneObject(target: any, persistOwnProps: boolean = true) {
const obj =
target && target.__super

View File

@ -1,7 +1,7 @@
import {ListenerAction, ListenerContext, runActions} from '../actions/Action';
import {RendererProps} from '../factory';
import {IScopedContext} from '../Scoped';
import {createObject} from './object';
import {createObject, extendObject} from './object';
import debounce from 'lodash/debounce';
export interface debounceConfig {
@ -65,7 +65,7 @@ export function createRendererEvent<T extends RendererEventContext>(
context: T
): RendererEvent<T> {
const rendererEvent = {
context,
context: extendObject({pristineData: context.data}, context),
type,
prevented: false,
stoped: false,
@ -81,6 +81,10 @@ export function createRendererEvent<T extends RendererEventContext>(
return rendererEvent.context.data;
},
get pristineData() {
return rendererEvent.context.pristineData;
},
setData(data: any) {
rendererEvent.context.data = data;
}
@ -128,10 +132,13 @@ export const bindEvent = (renderer: any) => {
});
}
}
return () => {
return (eventName?: string) => {
// eventName用来避免过滤广播事件
rendererEventListeners = rendererEventListeners.filter(
(item: RendererEventListener) => item.renderer !== renderer
(item: RendererEventListener) =>
item.renderer !== renderer && eventName !== undefined
? item.type !== eventName
: true
);
};
}
@ -147,7 +154,7 @@ export async function dispatchEvent(
data: any,
broadcast?: RendererEvent<any>
): Promise<RendererEvent<any> | void> {
let unbindEvent: (() => void) | null | undefined = null;
let unbindEvent: ((eventName?: string) => void) | null | undefined = null;
const eventName = typeof e === 'string' ? e : e.type;
renderer?.props?.env?.beforeDispatchEvent?.(
@ -183,6 +190,7 @@ export async function dispatchEvent(
data,
scoped
});
// 过滤&排序
const listeners = rendererEventListeners
.filter(
@ -198,7 +206,7 @@ export async function dispatchEvent(
const checkExecuted = () => {
executedCount++;
if (executedCount === listeners.length) {
unbindEvent?.();
unbindEvent?.(eventName);
}
};
for (let listener of listeners) {

View File

@ -1,6 +1,6 @@
{
"name": "amis-editor-core",
"version": "5.4.7",
"version": "5.5.0",
"description": "amis 可视化编辑器",
"main": "lib/index.js",
"module": "esm/index.js",

View File

@ -0,0 +1,45 @@
/**
* @file AsyncLayer.tsx
* @desc
*/
import React from 'react';
import {Spinner} from 'amis';
export interface asyncLayerOptions {
fallback?: React.ReactNode;
}
export const makeAsyncLayer = (
schemaBuilderFn: () => Promise<any>,
options?: asyncLayerOptions
) => {
const {fallback} = options || {};
const LazyComponent = React.lazy(async () => {
const schemaFormRender = await schemaBuilderFn();
return {
default: (...props: any[]) => <>{schemaFormRender(...props)}</>
};
});
return (props: any) => (
<React.Suspense
fallback={
fallback && React.isValidElement(fallback) ? (
fallback
) : (
<Spinner
show
overlay
size="sm"
tip="配置面板加载中"
tipPlacement="bottom"
className="flex"
/>
)
}
>
<LazyComponent {...props} />
</React.Suspense>
);
};

View File

@ -1291,6 +1291,8 @@ export class EditorManager {
}
store.changeValue(value, diff);
this.trigger('after-update', context);
}
/**

View File

@ -1,7 +1,10 @@
/**
* @file interface BasePlugin
*/
import omit from 'lodash/omit';
import {RegionWrapperProps} from './component/RegionWrapper';
import {makeAsyncLayer} from './component/AsyncLayer';
import {EditorManager} from './manager';
import {EditorStoreType} from './store/editor';
import {EditorNodeType} from './store/node';
@ -13,7 +16,7 @@ import find from 'lodash/find';
import type {RendererConfig} from 'amis-core';
import type {MenuDivider, MenuItem} from 'amis-ui/lib/components/ContextMenu';
import type {BaseSchema, SchemaCollection} from 'amis';
import {DSFieldGroup} from './builder/DSBuilder';
import type {asyncLayerOptions} from './component/AsyncLayer';
/**
*
@ -800,6 +803,11 @@ export interface PluginInterface
*/
panelJustify?: boolean;
/**
*
*/
async?: {enable: boolean} & asyncLayerOptions;
/**
*
*/
@ -1045,17 +1053,34 @@ export abstract class BasePlugin implements PluginInterface {
icon: plugin.panelIcon || plugin.icon || 'fa fa-cog',
pluginIcon: plugin.pluginIcon,
title: plugin.panelTitle || '设置',
render: this.manager.makeSchemaFormRender({
definitions: plugin.panelDefinitions,
submitOnChange: plugin.panelSubmitOnChange,
api: plugin.panelApi,
body: body,
controls: plugin.panelControlsCreator
? plugin.panelControlsCreator(context)
: plugin.panelControls!,
justify: plugin.panelJustify,
panelById: store.activeId
})
render:
typeof plugin.async === 'object' && plugin.async?.enable === true
? makeAsyncLayer(async () => {
const panelBody = await body;
return this.manager.makeSchemaFormRender({
definitions: plugin.panelDefinitions,
submitOnChange: plugin.panelSubmitOnChange,
api: plugin.panelApi,
body: panelBody,
controls: plugin.panelControlsCreator
? plugin.panelControlsCreator(context)
: plugin.panelControls!,
justify: plugin.panelJustify,
panelById: store.activeId
});
}, omit(plugin.async, 'enable'))
: this.manager.makeSchemaFormRender({
definitions: plugin.panelDefinitions,
submitOnChange: plugin.panelSubmitOnChange,
api: plugin.panelApi,
body: body,
controls: plugin.panelControlsCreator
? plugin.panelControlsCreator(context)
: plugin.panelControls!,
justify: plugin.panelJustify,
panelById: store.activeId
})
});
} else if (
context.info.plugin === this &&

View File

@ -1214,7 +1214,7 @@ export const updateComponentContext = (variables: any[]) => {
...child,
label:
index === 0
? `当前数据域${child.label ? '(' + child.label + ')' : ''}`
? `当前${child.label ? '(' + child.label + ')' : ''}`
: `${index}${child.label ? '(' + child.label + ')' : ''}`
}))
});

View File

@ -1,6 +1,6 @@
{
"name": "amis-editor",
"version": "5.4.7",
"version": "5.5.0",
"description": "amis 可视化编辑器",
"main": "lib/index.js",
"module": "esm/index.js",

View File

@ -117,7 +117,7 @@ import './plugin/Divider'; // 分隔线
import './plugin/CodeView'; // 代码高亮
import './plugin/Markdown';
import './plugin/Collapse'; // 折叠器
// import './plugin/OfficeViewer'; // 文档预览
import './plugin/OfficeViewer'; // 文档预览
import './plugin/Log'; // 日志
// 其他

View File

@ -1,6 +1,11 @@
import {getI18nEnabled, registerEditorPlugin} from 'amis-editor-core';
import {
RendererPluginEvent,
getI18nEnabled,
registerEditorPlugin
} from 'amis-editor-core';
import {BasePlugin, RegionConfig, BaseEventContext} from 'amis-editor-core';
import {defaultValue, getSchemaTpl} from 'amis-editor-core';
import {getEventControlConfig} from '../renderer/event-control/helper';
export class CollapsePlugin extends BasePlugin {
static id = 'CollapsePlugin';
@ -35,6 +40,31 @@ export class CollapsePlugin extends BasePlugin {
panelJustify = true;
events: RendererPluginEvent[] = [
{
eventName: 'change',
eventLabel: '折叠状态改变',
description: '折叠器折叠状态改变时触发',
dataSchema: [
{
type: 'object',
properties: {
data: {
type: 'object',
title: '数据',
properties: {
collapsed: {
type: 'boolean',
title: '折叠器状态'
}
}
}
}
}
]
}
];
panelBodyCreator = (context: BaseEventContext) => {
const i18nEnabled = getI18nEnabled();
return getSchemaTpl('tabs', [
@ -121,6 +151,16 @@ export class CollapsePlugin extends BasePlugin {
]
})
])
},
{
title: '事件',
className: 'p-none',
body: [
getSchemaTpl('eventControl', {
name: 'onEvent',
...getEventControlConfig(this.manager, context)
})
]
}
]);
};

View File

@ -1,9 +1,14 @@
import {getI18nEnabled, registerEditorPlugin} from 'amis-editor-core';
import {
RendererPluginEvent,
getI18nEnabled,
registerEditorPlugin
} from 'amis-editor-core';
import {BasePlugin, RegionConfig, BaseEventContext} from 'amis-editor-core';
import {defaultValue, getSchemaTpl} from 'amis-editor-core';
import {tipedLabel} from 'amis-editor-core';
import {isObject} from 'amis-editor-core';
import {getEventControlConfig} from '../renderer/event-control/helper';
export class CollapseGroupPlugin extends BasePlugin {
static id = 'CollapseGroupPlugin';
@ -56,6 +61,39 @@ export class CollapseGroupPlugin extends BasePlugin {
...this.scaffold
};
events: RendererPluginEvent[] = [
{
eventName: 'change',
eventLabel: '折叠状态改变',
description: '折叠面板折叠状态改变时触发',
dataSchema: [
{
type: 'object',
properties: {
data: {
title: '数据',
type: 'object',
properties: {
activeKeys: {
type: 'array',
title: '当前展开的索引列表'
},
collapseId: {
type: 'string',
title: '折叠器索引'
},
collapsed: {
type: 'boolean',
title: '折叠器状态'
}
}
}
}
}
]
}
];
activeKeyData: any = [];
panelTitle = '折叠面板';
@ -235,6 +273,16 @@ export class CollapseGroupPlugin extends BasePlugin {
isFormItem: false
})
])
},
{
title: '事件',
className: 'p-none',
body: [
getSchemaTpl('eventControl', {
name: 'onEvent',
...getEventControlConfig(this.manager, context)
})
]
}
])
];

View File

@ -966,7 +966,8 @@ export class FormPlugin extends BasePlugin {
? await current.info.plugin.buildDataSchemas(current, region)
: {
type: 'string',
title: schema.label || schema.name,
title:
typeof schema.label === 'string' ? schema.label : schema.name,
originalValue: schema.value // 记录原始值,循环引用检测需要
};
}

View File

@ -185,6 +185,7 @@ export class PickerControlPlugin extends BasePlugin {
]
},
getSchemaTpl('strictMode'),
getSchemaTpl('multiple'),
getSchemaTpl('joinValues'),
getSchemaTpl('delimiter'),

View File

@ -13,7 +13,7 @@ export class OfficeViewerPlugin extends BasePlugin {
name = '文档预览';
isBaseComponent = true;
description = 'Office 文档预览';
docLink = '/amis/zh-CN/components/OfficeViewer';
docLink = '/amis/zh-CN/components/office-viewer';
tags = ['展示'];
icon = 'fa fa-file-word';
pluginIcon = 'officeViewer-plugin';
@ -176,7 +176,9 @@ export class OfficeViewerPlugin extends BasePlugin {
},
{
title: '外观',
className: 'p-none'
body: getSchemaTpl('collapseGroup', [
getSchemaTpl('style:classNames', {isFormItem: false})
])
}
])
];

View File

@ -111,56 +111,63 @@ export default class APIAdaptorControl extends React.Component<
const lastParams =
typeof mergeParams === 'function' ? mergeParams(params) : params;
return render('api-adaptor-control-editor', [
{
type: 'container',
className: 'ae-AdaptorControl-func-header',
body: [
'<span class="mtk6">function&nbsp;</span>',
'<span class="mtk1 bracket-highlighting-0">(</span>',
...lastParams
.map(({label, tip}, index) => {
return [
{
type: 'button',
level: 'link',
label,
className: 'ae-AdaptorControl-func-arg',
...(tip ? {tooltip: this.genTooltipProps(tip)} : {})
},
...(index === lastParams.length - 1
? []
: ['<span class="mtk1">,&nbsp;</span>'])
];
})
.flat(),
'<span class="mtk1 bracket-highlighting-0">)&nbsp;{</span>'
]
},
{
label: '',
mode: 'normal',
name: '__editor_' + name,
type: 'js-editor',
className: 'ae-AdaptorControl-func-editor',
allowFullscreen,
value,
placeholder: editorPlaceholder || '',
onChange: (value: any) => {
this.onChange(value);
}
},
{
type: 'container',
body: '<span class="mtk1 bracket-highlighting-0">}</span>',
className: 'ae-AdaptorControl-func-footer'
},
{
type: 'container',
className: 'cxd-Form-description',
body: editorDesc
}
]);
return (
<>
{render('api-adaptor-control-editor/0', {
type: 'container',
className: 'ae-AdaptorControl-func-header',
body: [
'<span class="mtk6">function&nbsp;</span>',
'<span class="mtk1 bracket-highlighting-0">(</span>',
...lastParams
.map(({label, tip}, index) => {
return [
{
type: 'button',
level: 'link',
label,
className: 'ae-AdaptorControl-func-arg',
...(tip ? {tooltip: this.genTooltipProps(tip)} : {})
},
...(index === lastParams.length - 1
? []
: ['<span class="mtk1">,&nbsp;</span>'])
];
})
.flat(),
'<span class="mtk1 bracket-highlighting-0">)&nbsp;{</span>'
]
})}
{render(
'api-adaptor-control-editor/1',
{
label: '',
name: '__whatever_name_adpator',
placeholder: editorPlaceholder || '',
mode: 'normal',
type: 'js-editor',
className: 'ae-AdaptorControl-func-editor',
allowFullscreen
},
{
value,
onChange: this.onChange
}
)}
{render('api-adaptor-control-editor/2', {
type: 'container',
body: '<span class="mtk1 bracket-highlighting-0">}</span>',
className: 'ae-AdaptorControl-func-footer'
})}
{render('api-adaptor-control-editor/3', {
type: 'container',
className: 'cxd-Form-description',
body: editorDesc
})}
</>
);
}
renderSwitch() {

View File

@ -411,11 +411,7 @@ export default class APIControl extends React.Component<
tooltip: labelRemark,
className: cx(`Form-lableRemark`, labelRemark?.className),
useMobileUI,
container: popOverContainer
? popOverContainer
: env && env.getModalContainer
? env.getModalContainer
: undefined
container: popOverContainer || env.getModalContainer
})
: null}
</label>

View File

@ -158,11 +158,7 @@ export default class MapSourceControl extends React.Component<
tooltip: labelRemark,
className: cx(`Form-lableRemark`, labelRemark?.className),
useMobileUI,
container: popOverContainer
? popOverContainer
: env && env.getModalContainer
? env.getModalContainer
: undefined
container: popOverContainer || env.getModalContainer
})
: null}
</label>

View File

@ -197,11 +197,7 @@ export default class NavSourceControl extends React.Component<
tooltip: labelRemark,
className: cx(`Form-lableRemark`, labelRemark?.className),
useMobileUI,
container: popOverContainer
? popOverContainer
: env && env.getModalContainer
? env.getModalContainer
: undefined
container: popOverContainer || env.getModalContainer
})
: null}
</label>

View File

@ -513,11 +513,7 @@ export default class OptionControl extends React.Component<
tooltip: labelRemark,
className: cx(`Form-lableRemark`, labelRemark?.className),
useMobileUI,
container: popOverContainer
? popOverContainer
: env && env.getModalContainer
? env.getModalContainer
: undefined
container: popOverContainer || env.getModalContainer
})
: null}
</label>

View File

@ -360,11 +360,7 @@ export default class TimelineItemControl extends React.Component<
tooltip: labelRemark,
className: cx(`Form-lableRemark`, labelRemark?.className),
useMobileUI,
container: popOverContainer
? popOverContainer
: env && env.getModalContainer
? env.getModalContainer
: undefined
container: popOverContainer || env.getModalContainer
})
: null}
</label>

View File

@ -189,11 +189,7 @@ function BaseOptionControl(Cmpt: React.JSXElementConstructor<any>) {
tooltip: labelRemark,
className: cx(`Form-lableRemark`, labelRemark?.className),
useMobileUI,
container: popOverContainer
? popOverContainer
: env && env.getModalContainer
? env.getModalContainer
: undefined
container: popOverContainer || env.getModalContainer
})
: null}
</label>

View File

@ -192,11 +192,7 @@ export default class TreeOptionControl extends React.Component<
tooltip: labelRemark,
className: cx(`Form-lableRemark`, labelRemark?.className),
useMobileUI,
container: popOverContainer
? popOverContainer
: env && env.getModalContainer
? env.getModalContainer
: undefined
container: popOverContainer || env.getModalContainer
})
: null}
</label>

View File

@ -12,8 +12,8 @@ export const BASE_ACTION_PROPS = [
'__actionDesc',
'preventDefault',
'stopPropagation',
'expression',
'outputVar'
'expression'
// 'outputVar'
];
export default class CmptActionSelect extends React.Component<RendererProps> {

View File

@ -485,7 +485,7 @@ export const ACTION_TYPE_TREE = (manager: any): RendererPluginAction[] => {
)
},
{
name: 'args',
name: 'dialog',
label: '弹框内容',
mode: 'horizontal',
required: true,
@ -690,9 +690,9 @@ export const ACTION_TYPE_TREE = (manager: any): RendererPluginAction[] => {
actionLabel: '发送请求',
actionType: 'ajax',
description: '配置并发送API请求',
innerArgs: ['api', 'options'],
// innerArgs: ['api', 'options'],
descDetail: (info: any) => {
let apiInfo = info?.args?.api;
let apiInfo = info?.api ?? info?.args?.api;
if (typeof apiInfo === 'string') {
apiInfo = normalizeApi(apiInfo);
}
@ -711,43 +711,72 @@ export const ACTION_TYPE_TREE = (manager: any): RendererPluginAction[] => {
type: 'wrapper',
className: 'p-none',
body: [
getArgsWrapper(
[
getSchemaTpl('apiControl', {
name: 'api',
label: '配置请求',
mode: 'horizontal',
size: 'lg',
inputClassName: 'm-b-none',
renderLabel: true,
required: true
}),
// getArgsWrapper(
// [
// getSchemaTpl('apiControl', {
// name: 'api',
// label: '配置请求',
// mode: 'horizontal',
// size: 'lg',
// inputClassName: 'm-b-none',
// renderLabel: true,
// required: true
// }),
// {
// name: 'options',
// type: 'combo',
// label: tipedLabel(
// '静默请求',
// '开启后,服务请求将以静默模式发送,即不会弹出成功或报错提示。'
// ),
// mode: 'horizontal',
// items: [
// {
// type: 'switch',
// name: 'silent',
// label: false,
// onText: '开启',
// offText: '关闭',
// mode: 'horizontal',
// pipeIn: defaultValue(false)
// }
// ]
// }
// ],
// false,
// {
// className: 'action-apiControl'
// }
// ),
getSchemaTpl('apiControl', {
name: 'api',
label: '配置请求',
mode: 'horizontal',
size: 'lg',
inputClassName: 'm-b-none',
renderLabel: true,
required: true
}),
{
name: 'options',
type: 'combo',
label: tipedLabel(
'静默请求',
'开启后,服务请求将以静默模式发送,即不会弹出成功或报错提示。'
),
mode: 'horizontal',
items: [
{
name: 'options',
type: 'combo',
label: tipedLabel(
'静默请求',
'开启后,服务请求将以静默模式发送,即不会弹出成功或报错提示。'
),
type: 'switch',
name: 'silent',
label: false,
onText: '开启',
offText: '关闭',
mode: 'horizontal',
items: [
{
type: 'switch',
name: 'silent',
label: false,
onText: '开启',
offText: '关闭',
mode: 'horizontal',
pipeIn: defaultValue(false)
}
]
pipeIn: defaultValue(false)
}
],
false,
{
className: 'action-apiControl'
}
),
]
},
{
name: 'outputVar',
type: 'input-text',
@ -787,26 +816,35 @@ export const ACTION_TYPE_TREE = (manager: any): RendererPluginAction[] => {
actionLabel: '下载文件',
actionType: 'download',
description: '触发下载文件',
innerArgs: ['api'],
// innerArgs: ['api'],
schema: {
type: 'wrapper',
style: {padding: '0'},
body: [
getArgsWrapper(
getSchemaTpl('apiControl', {
name: 'api',
label: '配置请求',
mode: 'horizontal',
inputClassName: 'm-b-none',
size: 'lg',
renderLabel: true,
required: true
}),
false,
{
className: 'action-apiControl'
}
)
// getArgsWrapper(
// getSchemaTpl('apiControl', {
// name: 'api',
// label: '配置请求',
// mode: 'horizontal',
// inputClassName: 'm-b-none',
// size: 'lg',
// renderLabel: true,
// required: true
// }),
// false,
// {
// className: 'action-apiControl'
// }
// )
getSchemaTpl('apiControl', {
name: 'api',
label: '配置请求',
mode: 'horizontal',
inputClassName: 'm-b-none',
size: 'lg',
renderLabel: true,
required: true
})
]
}
}
@ -2738,11 +2776,13 @@ export const getEventControlConfig = (
config.__actionExpression = action.args?.value;
}
if (
action.actionType === 'ajax' &&
typeof action?.args?.api === 'string'
) {
action.args.api = normalizeApi(action?.args?.api);
if (['ajax', 'download'].includes(action.actionType)) {
config.api = action.api ?? action?.args?.api;
config.options = action.options ?? action?.args?.options;
if (typeof action?.api === 'string') {
config.api = normalizeApi(action?.api);
}
delete config.args;
}
// 获取动作专有配置参数

View File

@ -491,8 +491,12 @@ export class EventControl extends React.Component<
}
buildEventDataSchema(data: any, manager: EditorManager) {
const {actionTree, pluginActions, commonActions, allComponents} =
this.props;
const {
actionTree,
actions: pluginActions,
commonActions,
allComponents
} = this.props;
const {events, onEvent} = this.state;
const eventConfig = events.find(
@ -564,7 +568,7 @@ export class EventControl extends React.Component<
properties: {
...jsonSchema.properties?.data?.properties,
[action.outputVar!]: {
...actionSchema[0],
...(Array.isArray(actionSchema) && (actionSchema[0] || {})),
title: `${action.outputVar}(${actionLabel}动作出参)`
}
}

View File

@ -140,6 +140,25 @@ setSchemaTpl('multiple', (schema: any = {}) => {
};
});
setSchemaTpl('strictMode', {
type: 'switch',
label: '严格模式',
name: 'strictMode',
value: false,
mode: 'horizontal',
horizontal: {
justify: true,
left: 8
},
inputClassName: 'is-inline ',
labelRemark: {
trigger: ['hover', 'focus'],
setting: true,
title: '',
content: '启用严格模式将采用值严格相等比较'
}
});
setSchemaTpl('checkAllLabel', {
type: 'input-text',
name: 'checkAllLabel',

View File

@ -1,6 +1,6 @@
{
"name": "amis-formula",
"version": "3.1.5",
"version": "3.3.0-beta.4",
"description": "负责 amis 里面的表达式实现,内置公式,编辑器等",
"main": "lib/index.js",
"module": "esm/index.js",
@ -81,7 +81,6 @@
"browserslist": "IE >= 11",
"jest": {
"testEnvironment": "jsdom",
"collectCoverage": true,
"coverageReporters": [
"text",
"cobertura"

View File

@ -3,7 +3,7 @@
"main": "lib/index.js",
"module": "esm/index.js",
"types": "lib/index.d.ts",
"version": "3.1.5",
"version": "3.3.0-beta.4",
"description": "",
"scripts": {
"build": "npm run clean-dist && NODE_ENV=production rollup -c ",
@ -35,8 +35,8 @@
},
"dependencies": {
"@rc-component/mini-decimal": "^1.0.1",
"amis-core": "^3.1.1",
"amis-formula": "^3.1.1",
"amis-core": "^3.3.0-beta.4",
"amis-formula": "^3.3.0-beta.4",
"classnames": "2.3.1",
"codemirror": "^5.63.0",
"downshift": "6.1.12",
@ -108,7 +108,6 @@
},
"jest": {
"testEnvironment": "jsdom",
"collectCoverage": true,
"coverageReporters": [
"text",
"cobertura"

View File

@ -1,4 +1,5 @@
:root {
:root,
.AMISCSSWrapper {
--button-default-default-top-border-color: var(--colors-neutral-line-8);
--button-default-default-top-border-style: var(--borders-style-2);
--button-default-default-top-border-width: var(--borders-width-2);

View File

@ -5,7 +5,8 @@ $remFactor: 16px;
/* 此处放置需要override的变量因为部分变量已经在variables.scss中定义 */
$Table-strip-bg: transparent;
:root {
:root,
.AMISCSSWrapper {
--white: var(--colors-neutral-text-11);
--primary: var(--colors-brand-5);
--primary-onHover: var(--colors-brand-6);

View File

@ -198,6 +198,7 @@
width: px2rem(10px);
height: px2rem(10px);
top: 0;
transform: rotate(90deg);
}
}

View File

@ -543,6 +543,10 @@
margin-bottom: 0;
font-size: var(--fontSizeLg);
& + .#{$ns}Form-item-controlBox {
max-width: calc(100% - 28%);
}
> span {
line-height: px2rem(32px);
display: inline-block;
@ -677,6 +681,9 @@
.#{$ns}Form-item-controlBox {
flex: 1;
max-width: -moz-available;
max-width: -webkit-fill-available;
max-width: fill-available;
}
.#{$ns}Form-static {

View File

@ -445,6 +445,7 @@
width: px2rem(10px);
height: px2rem(10px);
top: 0;
transform: rotate(90deg);
}
}

View File

@ -314,16 +314,15 @@
}
}
&-sugs {
position: absolute;
&-popover {
margin-top: px2rem(4px);
background: var(--Form-select-menu-bg);
color: var(--Form-select-menu-color);
border-radius: px2rem(2px);
box-shadow: var(--menu-box-shadow);
left: px2rem(-1px);
right: px2rem(-1px);
top: calc(100% + #{px2rem(4px)});
z-index: 10;
}
&-sugs {
max-height: px2rem(300px);
overflow: auto;
}

View File

@ -147,9 +147,9 @@
&:hover {
.#{$ns}Tree {
&-itemLabel-item:not(.is-mobile) {
background-color: var(--Tree-item-onHover-bg-pure);
}
// &-itemLabel-item:not(.is-mobile) {
// background-color: var(--Tree-item-onHover-bg-pure);
// }
&-item-icons {
visibility: visible;
@ -163,6 +163,10 @@
}
&-item {
&:hover {
background-color: var(--Tree-item-onHover-bg-pure);
}
.is-checked {
border-radius: var(--Tree-item-onChekced-bg-borderRadius);
.#{$ns}Tree {
@ -211,7 +215,7 @@
}
&-itemInput {
padding-left: var(--Tree-itemArrowWidth);
// padding-left: var(--Tree-itemArrowWidth);
display: flex;
flex-direction: row;
flex-wrap: nowrap;

View File

@ -49,6 +49,7 @@ export interface CalendarMobileProps extends ThemeProps, LocaleProps {
};
};
defaultDate?: moment.Moment;
isEndDate?: boolean;
}
export interface CalendarMobileState {
@ -574,7 +575,8 @@ export class CalendarMobile extends React.Component<
viewMode = 'days',
close,
defaultDate,
showViewMode
showViewMode,
isEndDate
} = this.props;
const __ = this.props.translate;
@ -654,6 +656,7 @@ export class CalendarMobile extends React.Component<
hideHeader={true}
updateOn={viewMode}
key={'calendar' + index}
isEndDate={isEndDate}
/>
</div>
);
@ -671,7 +674,8 @@ export class CalendarMobile extends React.Component<
close,
timeConstraints,
defaultDate,
isDatePicker
isDatePicker,
isEndDate
} = this.props;
const __ = this.props.translate;
@ -705,6 +709,7 @@ export class CalendarMobile extends React.Component<
})}
timeConstraints={timeConstraints}
isValidDate={this.checkIsValidDate}
isEndDate={isEndDate}
/>
</div>
);

View File

@ -110,9 +110,10 @@ export class Collapse extends React.Component<CollapseProps, CollapseState> {
if (props.disabled || props.collapsable === false) {
return;
}
props.onCollapse && props.onCollapse(props, !this.state.collapsed);
const newCollapsed = !this.state.collapsed;
props.onCollapse?.(props, newCollapsed);
this.setState({
collapsed: !this.state.collapsed
collapsed: newCollapsed
});
}

View File

@ -28,10 +28,15 @@ export interface CollapseGroupProps {
classPrefix: string;
children?: React.ReactNode | Array<React.ReactNode>;
useMobileUI?: boolean;
onCollapseChange?: (
activeKeys: Array<string | number>,
collapseId: string | number,
collapsed: boolean
) => void;
}
export interface CollapseGroupState {
activeKey: Array<string | number | never>;
activeKeys: Array<string | number | never>;
}
class CollapseGroup extends React.Component<
@ -73,38 +78,43 @@ class CollapseGroup extends React.Component<
if (isInit) {
this.state = {
activeKey: curActiveKey.map((key: number | string) => String(key))
activeKeys: curActiveKey.map((key: number | string) => String(key))
};
} else {
this.setState({
activeKey: curActiveKey.map((key: number | string) => String(key))
activeKeys: curActiveKey.map((key: number | string) => String(key))
});
}
}
collapseChange(collapseId: string, collapsed: boolean) {
let activeKey = this.state.activeKey.concat();
let activeKeys = this.state.activeKeys.concat();
if (!collapsed) {
// 开启状态
if (this.props.accordion) {
activeKey = [];
activeKeys = [];
} else {
for (let i = 0; i < activeKey.length; i++) {
if (activeKey[i] === collapseId) {
activeKey.splice(i, 1); // 剔除开启状态
for (let i = 0; i < activeKeys.length; i++) {
if (activeKeys[i] === collapseId) {
activeKeys.splice(i, 1); // 剔除开启状态
break;
}
}
}
} else {
if (this.props.accordion) {
activeKey = [collapseId as string];
activeKeys = [collapseId as string];
} else {
activeKey.push(collapseId as string);
activeKeys.push(collapseId as string);
}
}
this.props.onCollapseChange?.(
activeKeys,
collapseId,
activeKeys.indexOf(collapseId) === -1
);
this.setState({
activeKey
activeKeys
});
}
@ -118,7 +128,7 @@ class CollapseGroup extends React.Component<
const collapseId = props.propKey || String(index);
// 判断是否折叠
const collapsed = this.state.activeKey.indexOf(collapseId) === -1;
const collapsed = this.state.activeKeys.indexOf(collapseId) === -1;
return React.cloneElement(child as any, {
...props,

View File

@ -310,6 +310,9 @@ export interface DateProps extends LocaleProps, ThemeProps {
onBlur?: Function;
onRef?: any;
data?: any;
// 是否为结束时间
isEndDate?: boolean;
}
export interface DatePickerState {
@ -708,6 +711,7 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
clearable,
shortcuts,
utc,
isEndDate,
overlayPlacement,
locale,
format,
@ -749,6 +753,7 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
viewMode === 'quarters' || viewMode === 'months' ? 'years' : 'months'
}
timeConstraints={timeConstraints}
isEndDate={isEndDate}
/>
);
const CalendarMobileTitle = (
@ -815,6 +820,7 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
onScheduleClick={onScheduleClick}
embed={embed}
useMobileUI={useMobileUI}
isEndDate={isEndDate}
/>
</div>
);
@ -902,6 +908,7 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
minDate={minDate}
maxDate={maxDate}
useMobileUI={useMobileUI}
isEndDate={isEndDate}
// utc={utc}
/>
</PopOver>

View File

@ -158,7 +158,7 @@ class DropDownSelection extends BaseSelection<
>
{!isMobile() ? (
<span className={cx('DropDownSelection-caret')}>
<Icon icon="caret" className="icon" />
<Icon icon="right-arrow-bold" className="icon" />
</span>
) : null}
</ResultBox>

View File

@ -18,6 +18,7 @@ import {themeable} from 'amis-core';
import {autobind, camel} from 'amis-core';
import {stripNumber} from 'amis-core';
import {isMobile} from 'amis-core';
import {safeAdd, safeSub} from 'amis-core';
import {findDOMNode} from 'react-dom';
import {Icon} from './icons';
@ -389,9 +390,9 @@ export class Range extends React.Component<RangeItemProps, any> {
let result = 0;
// 余数 >= 步长一半 -> 向上取
// 余数 < 步长一半 -> 向下取
const _value = surplus >= step / 2 ? value : value - step;
const _value = surplus >= step / 2 ? value : safeSub(value, step);
while (result <= _value) {
result += step;
result = safeAdd(result, step);
}
return result;
}

View File

@ -251,6 +251,7 @@ export class ResultBox extends React.Component<ResultBoxProps> {
maxTagCount,
overflowTagPopover,
showArrow,
popOverContainer,
...rest
} = this.props;
const isFocused = this.state.isFocused;

View File

@ -55,6 +55,7 @@ export default class TinymceEditor extends React.Component<TinymceEditorProps> {
};
config?: any;
editor?: any;
editorInitialized?: boolean = false;
currentContent?: string;
elementRef: React.RefObject<HTMLTextAreaElement> = React.createRef();
@ -136,6 +137,8 @@ export default class TinymceEditor extends React.Component<TinymceEditorProps> {
help: {title: 'Help', items: 'help'}
},
paste_data_images: true,
// 很诡异的问题video 会被复制放在光标上,直接用样式隐藏先
content_style: '[data-mce-bogus] video {display:none;}',
...this.props.config,
target: this.elementRef.current,
readOnly: this.props.disabled,
@ -144,6 +147,7 @@ export default class TinymceEditor extends React.Component<TinymceEditorProps> {
this.editor = editor;
editor.on('init', (e: Event) => {
this.editorInitialized = true;
this.initEditor(e, editor);
});
}
@ -159,7 +163,7 @@ export default class TinymceEditor extends React.Component<TinymceEditorProps> {
props.model !== prevProps.model &&
props.model !== this.currentContent
) {
this.editor?.setContent(props.model || '');
this.editorInitialized && this.editor?.setContent(props.model || '');
}
}

View File

@ -619,10 +619,11 @@ export class TreeSelector extends React.Component<
const result = [] as Option[];
for (let option of this.state.flattenedOptions) {
result.push(option);
if (option === parent) {
result.push({...option, isAdding: true});
} else {
result.push(option);
const insert = {isAdding: true};
this.levels.set(insert, (this.levels.get(option) || 0) + 1);
result.push(insert);
}
}
this.setState({flattenedOptions: result});
@ -888,12 +889,12 @@ export class TreeSelector extends React.Component<
if (!isVisible(item)) {
return;
}
this.levels.set(item, level);
if (paths.length === 0) {
// 父节点
flattenedOptions.push(item);
} else if (this.isUnfolded(parent)) {
this.relations.set(item, parent);
this.levels.set(item, level);
// 父节点是展开的状态
flattenedOptions.push(item);
}
@ -1125,7 +1126,9 @@ export class TreeSelector extends React.Component<
if (isEditing && editingItem === item) {
body = this.renderInput(checkbox);
} else if (item.isAdding) {
body = this.renderInput(checkbox);
body = this.renderInput(
<span className={cx('Tree-itemArrowPlaceholder')} />
);
} else {
body = (
<div
@ -1275,8 +1278,7 @@ export class TreeSelector extends React.Component<
})}
style={{
...style,
left: `calc(${level} * var(--Tree-indent))`,
width: `calc(100% - ${level} * var(--Tree-indent))`
paddingLeft: `calc(${level} * var(--Tree-indent))`
}}
>
{body}

View File

@ -238,7 +238,11 @@ export function withRemoteConfig<P = any>(
store.data,
'| raw'
),
() => this.syncConfig()
() => this.syncConfig(),
// 当nav配置source: "${amisStore.app.portalNavs}"时切换页面就会触发source更新
// 因此这里增加这个配置 数据源完全不相等情况下再执行loadConfig
// 否则数据源重置 保存不了展开状态 就会始终是手风琴模式了
{equals: comparer.structural}
)
);
} else if (env && isEffectiveApi(source, data)) {
@ -254,11 +258,7 @@ export function withRemoteConfig<P = any>(
ignoreData: true
}).url;
},
() => this.loadConfig(),
// 当nav配置source: "${amisStore.app.portalNavs}"时切换页面就会触发source更新
// 因此这里增加这个配置 数据源完全不相等情况下再执行loadConfig
// 否则数据源重置 保存不了展开状态 就会始终是手风琴模式了
{equals: comparer.structural}
() => this.loadConfig()
)
);
}

View File

@ -472,6 +472,11 @@ export class CustomDaysView extends React.Component<CustomDaysViewProps> {
});
// 最多展示3个
showSchedule = showSchedule.slice(0, 3);
const locale = this.props.viewDate.localeData();
// 以周几作为一周的开始0表示周日1表示周一
const firstDayOfWeek = locale.firstDayOfWeek();
const scheduleDiv = showSchedule.map((item: any, index: number) => {
let diffDays = moment(item.endTime).diff(
moment(item.startTime),
@ -486,9 +491,9 @@ export class CustomDaysView extends React.Component<CustomDaysViewProps> {
/* 前面的计算结果是闭区间所以最终结果要补足1 */
diffDays += 1;
const width =
item.width ||
Math.min(diffDays, 7 - moment(item.startTime).weekday());
const endWidth =
7 - (moment(item.startTime).weekday() - firstDayOfWeek + 1);
const width = item.width || Math.min(diffDays, endWidth) || 1;
return (
<div

View File

@ -127,7 +127,7 @@ export class ConditionFunc extends React.Component<ConditionFuncProps> {
disabled={disabled}
>
<span className={cx('CBGroup-fieldCaret')}>
<Icon icon="caret" className="icon" />
<Icon icon="right-arrow-bold" className="icon" />
</span>
</ResultBox>
</div>

View File

@ -260,7 +260,7 @@ export class ConditionGroup extends React.Component<
{body ? (
body.map((item, index) => (
<GroupOrItem
draggable={value!.children!.length > 1}
draggable={draggable && value!.children!.length > 1}
onDragStart={onDragStart}
config={config}
key={item.id}

View File

@ -248,7 +248,7 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
>
{!isMobile() ? (
<span className={cx('CBGroup-operatorCaret')}>
<Icon icon="caret" className="icon" />
<Icon icon="right-arrow-bold" className="icon" />
</span>
) : null}
</ResultBox>

View File

@ -37,6 +37,7 @@ export interface ConditionBuilderProps extends ThemeProps, LocaleProps {
onChange: (value?: ConditionGroupValue) => void;
config?: ConditionBuilderConfig;
disabled?: boolean;
draggable?: boolean;
searchable?: boolean;
fieldClassName?: string;
formula?: FormulaPickerProps;
@ -67,6 +68,9 @@ export class QueryBuilder extends React.Component<
@autobind
handleDragStart(e: React.DragEvent) {
const {draggable = true} = this.props;
// draggable为false时不可拖拽
if (!draggable) return;
const target = e.currentTarget;
const item = target.closest('[data-id]') as HTMLElement;
this.dragTarget = item;
@ -250,6 +254,7 @@ export class QueryBuilder extends React.Component<
showANDOR,
data,
disabled,
draggable = true,
searchable,
builderMode,
formula,
@ -291,6 +296,7 @@ export class QueryBuilder extends React.Component<
showNot={showNot}
data={data}
disabled={disabled}
draggable={draggable}
searchable={searchable}
formula={formula}
renderEtrValue={renderEtrValue}

View File

@ -80,8 +80,8 @@ export function FuncList(props: FuncListProps) {
className={cx('FormulaEditor-FuncList-item', {
'is-active': item.name === activeFunc?.name
})}
onClick={() => setActiveFunc(item)}
onDoubleClick={() => props.onSelect?.(item)}
onMouseEnter={() => setActiveFunc(item)}
onClick={() => props.onSelect?.(item)}
key={item.name}
>
{item.name}

Some files were not shown because too many files have changed in this diff Show More