项目地址: https://github.com/aisuda/amis-editor-demo 官方示例: https://aisuda.github.io/amis-editor-demo/ 说明文档: https://aisuda.bce.baidu.com/amis/zh-CN/docs/index 组件文档: https://aisuda.bce.baidu.com/amis/zh-CN/components/page

clone https://github.com/aisuda/amis-editor-demo.git
 
 
 
# 修改 amis.config.js
build: {
entry: { // webpack构建入口
  index: './src/index.tsx',
  // editor:  './src/mobile.tsx'
},
// 用于构建生产环境代码的相关配置信息
NODE_ENV: 'production',
assetsRoot: resolve('./demo'), // 打包后的文件绝对路径(物理路径)
assetsPublicPath: './', // 设置静态资源的引用路径(根域名+路径)[必须进行修改]
assetsSubDirectory: '', // 资源引用二级路径
productionSourceMap: false,
productionGzip: false,
productionGzipExtensions: ['js', 'css', 'json'],
plugins: [new MonacoWebpackPlugin()],
bundleAnalyzerReport: false,
}
 
# 安装依赖
npm install
 
# build
# IDEA 打开项目, 再文件目录右键"![[package.json]]" 文件, 打开"Show Npm Script", 左键"build
# 
 
# issue 1
# Module not found: Error: Can't resolve 'babel-loader' in 'D:\Project\OBS\amis-editor-demo'
# --save-dev 会更改 package.json 文件, 保存到对应位置上.
npm install --save-dev babel-loader
npm install --save-dev params-replace-loader
npm install --save-dev html-loader
npm install --save-dev ts-loader
npm install --save-dev babel-plugin-import
npm install --save-dev @babel/plugin-transform-runtime
npm install --save-dev @babel/plugin-proposal-decorators
npm install --save-dev @babel/plugin-proposal-function-sent
npm install --save-dev @babel/plugin-proposal-export-namespace-from
npm install --save-dev @babel/plugin-proposal-numeric-separator
npm install --save-dev @babel/plugin-proposal-throw-expressions
npm install --save-dev @babel/plugin-syntax-dynamic-import
npm install --save-dev @babel/plugin-syntax-import-meta
npm install --save-dev @babel/plugin-proposal-class-properties
npm install --save-dev @babel/plugin-proposal-json-strings
npm install --save-dev @babel/preset-env
npm install --save-dev @babel/preset-react
npm install --save-dev @babel/preset-typescript
npm install --save-dev vue-style-loader
npm install --save-dev css-loader
npm install --save-dev postcss-loader
npm install --save-dev deep-diff
npm install --save-dev tinycolor2
npm install --save-dev react-color
npm install --save-dev @rc-component/mini-decimal
npm install --save-dev @svgr/webpack
npm install --save-dev sass-loader
npm install --save-dev 
npm install --save-dev 
npm install --save-dev 
将打包后的文件复制到nginx的html目录的自建文件夹下,我的是dist文件夹。
 

api

https://aisuda.bce.baidu.com/amis/zh-CN/docs/types/api

  • 返回的数据需要是固定的格式{“status”: xxx, “msg”: xxx}
  • 当我们需要显示返回的信息的时候, 就需要配置请求的返回适配器, 将返回内容转换成这个格式.
status:payload.status === 0 ? 0 : 1,
msg: payload.msg || '系统错误',
data: payload.data

button

https://aisuda.bce.baidu.com/amis/zh-CN/components/button 实际上是action的别名 https://aisuda.bce.baidu.com/amis/zh-CN/components/action

ajax

请求成功后刷新目标组件

https://aisuda.bce.baidu.com/amis/zh-CN/components/action?page=1#ajax-%E8%AF%B7%E6%B1%82

目标组件需要配置 name 属性 Action 上添加 “reload”: “xxx”,xxx 为目标组件的 name 属性值,如果配置多个组件,name 用逗号分隔,另外如果想让 reload 的时候再携带些数据可以类似这样配置 {“reload”: “xxx?a={a}&b={b}”}, 这样不仅让目标组件刷新,同时还会把当前环境中的数据 a 和 b 传递给 xxx.

{
  "type": "page",
  "body": [
    {
      "type": "button",
      "label": "ajax 请求刷新目标",
      "actionType": "ajax",
      "api": "/amis/api/mock2/form/saveForm",
      "reload": "crud"
    },
    {
      "type": "divider"
    },
    {
      "type": "crud",
      "name": "crud",
      "api": "/amis/api/mock2/sample?waitSeconds=1",
      "columns": [
        {
          "name": "id",
          "label": "ID"
        }
      ]
    }
  ]
}

New page to download pdf file.

{
  "type": "button",
  "actionType": "url",
  "disabledOnAction": false,
  "size": "xs",
  "blank":"true",
  "level": "primary",
  "label": "生成新尽职调查报告",
  "id": "u:16ff40609919",
  "link": "/Document/getDueDiligenceReport/${instance_id}"
}

刷新页面

  • 表单按钮, 点击提交后重新加载表单.
{
  "type": "button",
  "label": "提交",
  "onEvent": {
    "click": {
      "actions": [
        {
          "actionType": "submit",
          "componentId": "u:a268ff5c574c",
          "reload": "u:a268ff5c574c"
        }
      ]
    }
  },
  "level": "primary",
  "id": "u:fbef13292ab4",
  "disabledOnAction": false
}

下载内容

{
  "type": "page",
  "body": {
    "label": "下载",
    "type": "action",
    "actionType": "download",
    "api": "/amis/api/download"
  }
}

提交请求, 返回内容

提交后不重置表单

默认提交表单后会重置表单内容, 开启此项配置, 保证内容保留在表单上.

"resetAfterSubmit": false

calendar

https://aisuda.bce.baidu.com/amis/zh-CN/components/calendar

curd

https://aisuda.bce.baidu.com/amis/zh-CN/components/crud

添加组件

  • 这个也就是数据容器中的”增删改查”

  1. 将组件拖进内容区之后, 在”常规”里面可以开启搜索框.
  2. 在 2 小锤子这个里面可以配置大部分接口调用的内容.
  3. 在查询条件的位置绑定查询字段名称.

配置接口

  • 发送数据的第一个参数是框架默认的, 如下图, 就是用来分页操作的.

配置展示字段

  1. 配置完接口之后, 可以使用 “校验格式并自动生成列配置”, 会自动绑定下面字段, 只需要改标题即可, 注意接口返回的数据需要按照如下格式.

  • 当行数据中存在 children 字段时,CRUD 会自动识别为树形数据,并支持展开收起。
{
    "status": "0",
    "msg": "处理成功",
    "data": {
        "perPage": "10",
        "page": "1",
        "controller": "pageSchema",
        "action": "getPageSchemaList",
        "items": [
            {
                "id": 1414,
                "jsonSchema": null,
                "name": "11",
                "description": "11",
                "children":[
                                "id": 1414,
				                "jsonSchema": null,
				                "name": "11",
				                "description": "11"
                ]
            }
        ],
        "total": 1
    }
}
def getMenuList() {  
    // 分页参数处理  
    int page = params.int('page') ?: 1  
    int perPage = params.int('perPage') ?: 10  
  
    String sql = """  
                  SELECT                  m.id, m.name, m.url, m.status, m.order_number, m.parent_id,    
                  ps.name as page_schema_name   
                  FROM menu m   
                  LEFT JOIN page_schema ps ON m.page_schema_id = ps.id   
                  WHERE 1=1 AND m.parent_id is null   
    """  
  
    Map paramsMap = new HashMap()  
  
    // 动态添加条件  
    String name = params.name  
    if (name) {  
        sql += " AND m.name LIKE :name "  
        paramsMap.name = "%" + name + "%"  
    }  
  
    // 动态添加条件  
    String url = params.url  
    if (url) {  
        sql += " AND m.url LIKE :url "  
        paramsMap.url = "%" + url + "%"  
    }  
  
    sql += " ORDER BY m.order_number ASC"  
  
    def sql1 = new Sql(dataSource)  
    def parentList = sql1.rows(sql, paramsMap, (page - 1) * perPage, perPage)  
    params.items = parentList.collect { parent ->  
        def children = sql1.rows("""  
                                  SELECT                                  m.id, m.name, m.url, m.status, m.order_number, m.parent_id,                                  ps.name as page_schema_name                                  FROM menu m                                  LEFT JOIN page_schema ps ON m.page_schema_id = ps.id                                  WHERE 1=1 AND m.parent_id = :parentId                                  ORDER BY m.order_number ASC                                  """, [parentId: parent.id])  
        parent.children = children  
        return parent  
    }  
  
    params.total = sql1.rows(sql, paramsMap).size()  
  
    def res = ResService.success(params)  
    render res as JSON  
}

这里配置单独的创建接口,

在这里设计表单, 创建内容

找到这个控件, 注意提交编辑的时候, 要带上本条记录的id.

给这个控件配置信息

自定义按钮

  • 自定义的功能按钮,可以配置到CURD组件的“新增”按钮位置,通过如下内容访问指定功能,然后reload下面的CURD列表(注意下面的CURD列表需要配置name属性为“740list”)。
{
  "type": "button",
  "actionType": "ajax",
  "disabledOnAction": false,
  "reload": "740list",
  "size": "xs",
  "level": "primary",
  "label": "同步数据",
  "id": "u:36435c28df7a",
  "api": "/Account/sync740List/${id}"
}

date

{
  "type": "date",
  "format": "YYYY年MM月DD日 HH时mm分ss秒",
  "label": "修改时间",
  "name": "modified_date",
  "id": "u:182e02cc1735",
  "placeholder": "-"
}

dialog

https://aisuda.bce.baidu.com/amis/zh-CN/components/dialog?page=1

当curd的列表页要显示详情的时候,这个时候通常会 use a dialog to show this. How to pass a value to the dialog? Open the data map option, set the value like this: then you can use user_id from the dialog, and its value equal the id from the curd component.

  "dataMap": {
    "user_id": "${id}"
  },

event-action 事件动作

https://aisuda.bce.baidu.com/amis/zh-CN/docs/concepts/event-action

执行JS

/* 自定义JS使用说明:
* 1.动作执行函数doAction,可以执行所有类型的动作
* 2.通过上下文对象context可以获取当前组件实例,例如context.props可以获取该组件相关属性
* 3.事件对象event,在doAction之后执行event.stopPropagation();可以阻止后续动作执行
*/
const myMsg = '我是自定义JS';
console.log(context.props); // 这里可以获取page上定义个data,还有amis初始化时候的data和context内容。
doAction({
	actionType: 'toast',
	args: {
	msg: myMsg
	}
});
      

initApi

在页面初始化的时候, 传递一些权限数据, 来控制部分功能的显示和隐藏. visibleOn

{
  "msg": "处理成功",
  "data": {
    "controller": "Opportunity",
    "action": "initData",
    "roles": [
      "ROLE_ADMIN",
      "ROLE_ATTACHMENTS",
      "ROLE_STATISTIC",
      "ROLE_DUE_DILIGENCE"
    ]
  },
  "status": 0
}

input-date

  • valueFormat 用毫秒时间戳的格式传递给后端
{
  "type": "input-date",
  "name": "expired_date",
  "label": "过期日期",
  "id": "u:0202b80811b4",
  "placeholder": "请选择日期",
  "displayFormat": "YYYY/MM/DD",
  "valueFormat": "x",
  "minDate": "",
  "maxDate": "",
  "value": "",
  "format": "X",
  "for": "X",
  "utc": false
}

input-datetime

  1. 一般用日期时间来显示
  2. 前端传递给后端固定格式的字符串: YYYY-MM-DD HH:mm:ss
  3. 前端显示的时候可以只显示日期, 或者显示日期和时间.
  4. 后端只需要解析固定格式的即可.
{
  "type": "input-datetime",
  "name": "credit_period",
  "label": "授信期限",
  "id": "u:1f3c40c67375",
  "keyboard": true,
  "showSteps": true,
  "step": 1,
  "valueFormat": "YYYY-MM-DD HH:mm:ss",
  "placeholder": "请选择日期以及时间",
  "displayFormat": "YYYY-MM-DD",
  "minDate": "",
  "maxDate": "",
  "value": ""
}

input-file

https://aisuda.bce.baidu.com/amis/zh-CN/components/form/input-file

input-time-range

  • 留意传递给后端的数据值
{
  "type": "input-datetime-range",
  "label": "创建时间",
  "name": "createTime",
  "id": "u:c3431b9fc961",
  "displayFormat": "YYYY-MM-DD HH:mm:ss",
  "placeholder": "请选择日期时间范围",
  "valueFormat": "YYYY-MM-DD HH:mm:ss",
  "minDate": "",
  "maxDate": "",
  "value": "",
  "shortcuts": [
    "yesterday",
    "7daysago",
    "prevweek",
    "thismonth",
    "prevmonth",
    "prevquarter"
  ]
}

link

https://aisuda.bce.baidu.com/amis/zh-CN/components/link

  • 可以放置在Table里面, 数据超连接到详情页面.
{
  "type": "link",
  "name": "name",
  "label": "名称",
  "id": "u:dd0ed10eb3da",
  "placeholder": "-",
  "href": "/PageSchema/edit#/edit/${id}",
  "body": "${name}"
}

image

  • 注意”可见”属性, ${里面是JavaScript表达式}, 通过结果返回的 true 和 false 来控制当前控件的显示和隐藏.
{
  "type": "image",
  "id": "u:0c7390b4ae3c",
  "enlargeAble": true,
  "maxScale": 200,
  "minScale": 50,
  "style": {
    "display": "inline-block"
  },
  "imageMode": "original",
  "src": "${file_url}",
  "visibleOn": "${extension == 'jpg'}"
}

form 表单

https://aisuda.bce.baidu.com/amis/zh-CN/components/form/index

group 表单组

https://aisuda.bce.baidu.com/amis/zh-CN/components/form/group

在水平模式下, 更美观的展示表单内容.

{
  "type": "group",
  "body": [
	{
	  "type": "input-password",
	  "name": "password",
	  "label": "密码",
	  "placeholder": "请输入密码",
	  "size": "full"
	}
  ]
}

markdown

目前markdown组件只能静态绑定到某个input组件上,当input组件内容发生变化之后,markdown区域同步更新。

示例:设定两个input组件,一个输入问题,一个显示返回结果(隐藏),form提交问题到后端,设置form事件,将返回结果绑定到global变量,隐藏的input也绑定到这个全局变量,最后markdown组件通过name绑定到隐藏的input,这时提交问题后返回的数据就动态呈现到markdown区域。

{
  "type": "page",
  "pullRefresh": {
    "disabled": true
  },
  "regions": [
    "body"
  ],
  "asideResizor": false,
  "id": 3079,
  "body": [
    {
      "labelAlign": "left",
      "onEvent": {
        "submitSucc": {
          "weight": 0,
          "actions": [
            {
              "args": {
                "value": "event.data.result.data.output",
                "path": "global.output"
              },
              "actionType": "setValue"
            }
          ]
        }
      },
      "dsType": "api",
      "style": {
        "padding": "10px",
        "borderTop": "1px solid [[eee]]"
      },
      "id": "u:7ec67e6e3a8f",
      "api": {
        "method": "post",
        "dataType": "form",
        "adaptor": "",
        "messages": {},
        "requestAdaptor": "",
        "url": "/AI/askAI"
      },
      "type": "form",
      "title": "",
      "body": [
        {
          "minRows": 3,
          "maxRows": 20,
          "name": "input",
          "id": "u:ab83d89c2862",
          "label": false,
          "placeholder": "请输入您的问题...",
          "type": "textarea",
          "rows": 2,
          "required": true
        },
        {
          "minRows": 3,
          "maxRows": 20,
          "name": "output",
          "id": "u:614de330d37b",
          "label": false,
          "placeholder": "请输入您的问题...",
          "type": "textarea",
          "rows": 2,
          "value": "${global.output}",
          "hidden": true
        },
        {
          "type": "markdown",
          "id": "u:69a4d217cd19",
          "name": "output"
        }
      ],
      "feat": "Insert",
      "actions": [
        {
          "disabledOnAction": false,
          "size": "xs",
          "level": "primary",
          "onEvent": {
            "click": {
              "actions": []
            }
          },
          "label": "发送",
          "id": "u:2a84cb573ff6",
          "type": "submit"
        }
      ],
      "wrapWithPanel": true,
      "debug": false
    }
  ]
}

nav

  • 如果想从后端接口获取多级菜单, 需要匹配当前接口的默认格式, 你需要再后端生成如下的接口返回数据.
{
    "status": 0,
    "msg": "请求成功",
    "data": [
        {
            "label": "首页",
            "to": "/home"
        },
        {
            "label": "产品",
            "to": "/products",
            "children": [
                {
                    "label": "产品1",
                    "to": "/products/product1"
                },
                {
                    "label": "产品2",
                    "to": "/products/product2"
                }
            ]
        },
        {
            "label": "关于我们",
            "to": "/about"
        }
    ]
}

page

"padding": "0.75rem", // 有的时候内容会紧贴着边,这时候要设置page的内边距属性。

onEvent 事件

https://aisuda.bce.baidu.com/amis/zh-CN/components/page#%E4%BA%8B%E4%BB%B6%E8%A1%A8

添加页面onEvent,可以配置init,inited,pullRefresh情况下的事件例如下面情况:

  • 需求是页面上一个按钮“小助手”,点击之后弹出一个聊天窗口,这个窗口用Service来从后端去pageJSON,这个取到的json就是一个page,这个page就需要包含聊天使用的ws服务初始化服务,这里就需要使用init来执行JS进行初始化这个ws服务。
// 从当前页面URL中提取基础信息,手动构造WebSocket地址
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; // 协议转换:https→wss,http→ws
const host = window.location.host; // 包含域名和端口(如:localhost:8080)
const contextPath = ''; // 若项目有上下文路径(如 /app),需手动添加,否则留空
const stompEndpoint = '/stomp'; // STOMP服务的端点路径
 
// 拼接完整的WebSocket URL
const brokerURL = `${protocol}//${host}${contextPath}${stompEndpoint}`;
 
const stompClient = new StompJs.Client({
    brokerURL: brokerURL,
    onConnect: () => {
        console.log('STOMP连接成功');
        // 订阅消息主题
        stompClient.subscribe('/topic/hello', (response) => {
            try {
                // 尝试解析 JSON 格式(兼容未来可能的 JSON 响应)
                const message = JSON.parse(response.body);
                addMessageToChat({ content: message.content }, false);
            } catch (e) {
                // 解析失败时直接将响应体作为文本内容处理
                addMessageToChat({ content: response.body }, false);
            }
        });
    },
    onStompError: (frame) => {
        console.error('STOMP错误: ' + frame.headers['message']);
    },
    reconnectDelay: 3000 // 自动重连延迟(毫秒)
});
 
// 保存到全局(关键:让后续操作能访问)
window.stompClient = stompClient;
stompClient.activate();
// 监听消息
stompClient.onmessage = function (event) {
	console.log('收到消息:', event.data);
	// 可通过 this.setState 更新 amis 页面数据
	this.setState({ message: event.data });
}.bind(this); // 绑定当前组件上下文,方便更新状态

select 下拉框

https://aisuda.bce.baidu.com/amis/zh-CN/components/form/select

def selectPositionList() {  
    String sql = """  
        select p.id, d.name as department_name, p.name as position_name, p.description        from position p join department d on p.department_id =d.id  
        where 1=1    """  
    Map paramsMap = new HashMap()  
    // 搜索关键词  
    String term = params.term  
    if (term) {  
        sql += " AND p.name LIKE :term "  
        paramsMap.term = "%" + term + "%"  
    }  
    sql += " ORDER BY p.id DESC"  
    def sql1 = new Sql(dataSource)  
    params.items = sql1.rows(sql, paramsMap)  
    def res = ResService.success(params)  
    render res as JSON  
}

多个下拉框的级联选择

// The first select component, config the change event, let it to reload the second select component.
{
  "type": "select",
  "strictMode": true,
  "name": "province_id",
  "label": "省份",
  "id": "province_id",
  "syncFields": [],
  "placeholder": "文本",
  "multiple": false,
  "source": "/Province/select",
  "labelField": "name",
  "valueField": "id",
  "onEvent": {
    "change": {
      "weight": 0,
      "actions": [
        {
          "actionType": "reload",
          "componentId": "city_id"
        }
      ]
    }
  }
}
 
// The second component, through the reload event, get new data with the first select component value.
{
  "type": "select",
  "strictMode": true,
  "name": "city_id",
  "label": "城市",
  "id": "city_id",
  "syncFields": [],
  "placeholder": "文本",
  "multiple": false,
  "labelField": "name",
  "valueField": "id",
  "source": "/City/select?province_id=${province_id}",
  "onEvent": {
    "change": {
      "weight": 0,
      "actions": [
        {
          "componentId": "district_id",
          "ignoreError": false,
          "actionType": "reload"
        }
      ]
    }
  }
}

service

https://aisuda.bce.baidu.com/amis/zh-CN/components/service

有一些组件没有获取数据的功能, 例如table-view, 可以放在一个数据服务组件内, 通过数据服务获取数据, 然后下级的组件来展示这些数据.

动态加载页面

例如在某个弹窗中get schema from the backend, then draw the content.

{
  "type": "service",
  "initFetchSchema": true,
  "dsType": "api",
  "schemaApi": {
    "method": "get",
    "adaptor": "",
    "messages": {},
    "requestAdaptor": "",
    "url": "/PageSchema/getSchemaByProduct/${id}"
  },
  "id": "u:dc2d061ac907",
  "body": []
}

动态加载页面的initApi问题

再CURD中”查看“功能,是通过 dialog 来动态加载一个page,但是这个page的initApi没有被正确执行,不知道为什么? 这里我们在dialog中先加一个service来获取initApi的数据,然后再该service的body再加一个service来获取动态的页面。 这时候通过数据链的特性,就可以使用initApi获取到的数据了。

steps

https://aisuda.bce.baidu.com/amis/zh-CN/components/steps

展示阶段信息

{
  "type": "form",
  "mode": "flex",
  "static": true,
  "labelAlign": "top",
  "dsType": "api",
  "initApi": {
    "method": "post",
    "data": {
      "workflow_instance_id": 1535,
      "&": "$$"
    },
    "dataType": "form",
    "adaptor": "",
    "messages": {},
    "requestAdaptor": "",
    "url": "/WorkflowInstance/getStepsAndUsers"
  },
  "id": "u:2905a3412127",
  "title": "",
  "body": [
    {
      "mode": "horizontal",
      "visible": true,
      "iconPosition": false,
      "name": "step",
      "id": "u:90f9c0b8f575",
      "source": "${steps}",
      "type": "steps",
      "value": "${value}",
      "steps": [
        {
          "type": "wrapper",
          "body": "子节点内容"
        },
        {
          "type": "wrapper",
          "body": "子节点内容"
        }
      ],
      "status": "process"
    }
  ],
  "feat": "View",
  "actions": []
}
def getStepsAndUsers(){
        Long id = params.workflow_instance_id as Long
        WorkflowInstance workflowInstance = WorkflowInstance.findById(id)
        if (id) {
            String sql = """
                    select wis.name, wis.start_time 
                    from workflow_instance_stage wis 
                    where 1=1 
                    
                """
            Map paramsMap = new HashMap()
            sql += " AND wis.instance_id = :id order by wis.execution_sequence asc;"
            paramsMap.id = id
            def sql1 = new Sql(dataSource)
            def record = sql1.rows(sql, paramsMap)
            Integer current = WorkflowInstanceStage.findAllByInstanceAndExecutionSequenceLessThan(workflowInstance,workflowInstance.stage.executionSequence).size()
            def steps = []
            record.each {
                def step = [:]
                step.put("title", it.name)
                step.put("subTitle", it.start_time)
                steps.add(step)
            }
            def res = ResService.success([steps: steps, value: current])
            render res as JSON
        } else {
            def res = ResService.fail("参数错误: workflow_instance_id 不能为空")
            render res as JSON
        }
    }

textarea

https://aisuda.bce.baidu.com/amis/zh-CN/components/form/textarea

tabs

https://aisuda.bce.baidu.com/amis/zh-CN/components/tabs 选项卡, 在选项卡里面添加内容, 先清空”内容”, 然后再复制组件进去.

timeline

https://aisuda.bce.baidu.com/amis/zh-CN/components/timeline

transfer-picker

从左边选择到右边的功能, 配合表单使用,

常用属性

visibleOn

然后使用组件的visiableOn属性来控制显示: 编辑器右侧的 状态可见表达式ARRAYSOME(roles, item => item === 'ROLE_DUE_DILIGENCE')

"visibleOn": "${ARRAYSOME(roles, item => item === 'ROLE_DUE_DILIGENCE')}"

注意要控制Tab的时候,要手动添加到某个Tab上。

APP 多页面

{
"label": "页面B", 
"badge": 3, // 菜单右侧的脚标
"badgeClassName": "bg-info",
"schema": {
  "type": "page",
  "title": "页面B",
  "body": "页面B"
}
},