diff --git a/.gitignore b/.gitignore
index b56c835d..d0f4d17a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,7 +3,6 @@
.DS_Store
yarn-error.log
package-lock.json
-docs/
.vscode
test/e2e/videos
test/e2e/screenshots
diff --git a/.prettierignore b/.prettierignore
index 007ea8a7..11bbdac7 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,3 +1,4 @@
dist
node_modules
coverage
+docs
diff --git a/docs/zh/develop/1-ready-to-work.md b/docs/zh/develop/1-ready-to-work.md
new file mode 100644
index 00000000..7e0f0722
--- /dev/null
+++ b/docs/zh/develop/1-ready-to-work.md
@@ -0,0 +1,94 @@
+简体中文 | [English](/docs/en/develop/1-ready-to-work.md)
+
+# 开发前准备
+
+- node 环境
+ - package.json 中要求:`"node": ">=10.22.0"`
+ - 验证 nodejs 版本
+
+ ```shell
+ node -v
+ ```
+
+- yarn
+ - 安装 yarn
+
+ ```shell
+ npm install -g yarn
+ ```
+
+- 安装依赖包
+ - 在项目根目录下执行,即`package.json`同级,需要耐心等待安装完成
+
+ ```shell
+ yarn install
+ ```
+
+- 准备好可用的后端
+ - 准备好可访问的后端,举个例子:https://172.20.154.250
+ - 修改`config/webpack.dev.js`中的相应配置:
+
+ ```javascript
+ if (API === 'mock' || API === 'dev') {
+ devServer.proxy = {
+ '/api': {
+ target: 'https://172.20.154.250',
+ changeOrigin: true,
+ secure: false,
+ },
+ };
+ }
+ ```
+
+- 配置访问的 host 与 port
+ - 修改`devServer.host`与`devServer.port`
+ - 修改`config/webpack.dev.js`中的相应配置
+
+ ```javascript
+ const devServer = {
+ host: '0.0.0.0',
+ // host: 'localhost',
+ port: 8088,
+ contentBase: root('dist'),
+ historyApiFallback: true,
+ compress: true,
+ hot: true,
+ inline: true,
+ disableHostCheck: true,
+ // progress: true
+ };
+ ```
+
+- 搭建完成
+ - 在项目根目录下执行,即`package.json`同级
+
+ ```shell
+ yarn run dev
+ ```
+
+ - 使用`config/webpack.dev.js`中配置的`host`与`port`访问即可,如`http://localhost:8088`
+ - 开发使用的前端实时更新环境搞定。
+
+# 生产环境使用的前端包
+
+- 具备符合要求的`nodejs`与`yarn`
+- 在项目根目录下执行,即`package.json`同级
+
+ ```shell
+ yarn run build
+ ```
+
+- 打包后的文件在`dist`目录,交给部署相关人员即可。
+
+# 测试使用的前端包
+
+- 具备符合要求的`nodejs`与`yarn`
+- 在项目根目录下执行,即`package.json`同级
+
+ ```shell
+ yarn run build:test
+ ```
+
+- 打包后的文件在`dist`目录
+- 注意!!!这个测试包为了测出代码覆盖率的
+- 建议使用 nginx,以完成带有代码覆盖率的 E2E 测试。
diff --git a/docs/zh/develop/2-catalog-introduction.md b/docs/zh/develop/2-catalog-introduction.md
new file mode 100644
index 00000000..52a6cc31
--- /dev/null
+++ b/docs/zh/develop/2-catalog-introduction.md
@@ -0,0 +1,380 @@
+简体中文 | [English](/docs/en/develop/2-catalog-introduction.md)
+
+# 一级目录简介
+
+- `Gruntfile.js`:用于收集 i18n
+- `LICENSE`: 该项目使用 Apache License
+- `Makefile`:
+- `README.md`: 前端启动的简单说明,详细信息请参考 docs 文档
+- `config`目录: webpack 配置,其内包含公用、开发环境、测试环境、生成环境下的 webpack 配置
+- `cypress.json`: e2e 测试的配置文件
+- `docker`: 内含开发环境、生成环境、测试环境使用的 docker 配置
+- `docs`目录: 文档介绍,包含中文、英文、开发说明文档、测试说明文档,其中 en 文档暂缺失
+- `jest.config.js`: 单元测试的配置文件
+- `jsconfig.json`: js 代码的配置文件
+- `package.json`: 安装包、命令等配置文件
+- `yarn.lock`: 包的版本锁定文件
+- `.babelrc`: bebel 配置文件
+- `.dockerignore`: docker 忽略的文件配置
+- `.eslintignore`: eslint 忽略的文件配置
+- `.eslint`: eslint 配置
+- `.gitignore`: git 忽悠的文件配置
+- `.gitreview`: gitreview 配置
+- `.prettierignore`: prettier 忽略的文件配置
+- `.prettierrc`: prettier 的配置
+- `src`目录: 开发代码所在文件夹!!!
+- `test`目录: 测试代码所在文件夹!!!包含 e2e 测试代码及单元测试的基础代码
+- `tools`目录: 其他工具文件夹,内含 git 工具
+
+# src 目录介绍
+
+- `src/components`目录:公共组件
+- `src/api`目录:API,暂未使用
+- `src/asset`目录:images, template 等静态文件
+- `src/containers`目录:
+ - 带状态的组件
+ - 基础类
+ - [BaseList](3-1-BaseList-introduction.md)
+ - [BaseDetail](3-3-BaseDetail-introduction.md)
+ - [BaseForm](3-6-FormAction-introduction.md)
+ - [BaseModalAction](3-7-ModalAction-introduction.md)
+ - [BaseConfirmAction](3-8-ConfirmAction-introduction.md)
+ - [BaseStepAction](3-9-StepAction-introduction.md)
+- `src/core`目录:
+ - `index.js`: 入口文件
+ - `routes.js`: 按模块的路由配置
+ - `i18n.js`
+ - `App.jsx`
+- `src/layouts`目录:
+ - 定义所有整体页面布局的组件
+ - 空白布局 BlankLayout
+ - 登录页使用的布局 UserLayout
+ - 内容页使用的布局 BaseLayout(列表、详情、表单等使用)
+ - `menu.jsx`: 控制台使用的菜单配置
+ - `admin-menu.jsx`: 管理平台使用的菜单配置
+- `src/locales`目录: i18n
+- `src/resources`目录:
+ - 定义各资源被公用的状态 / 搜索项
+ - 定义各资源被公用的表格列
+ - 定义各资源的复用函数
+- `src/stores`目录:
+ - 对资源的数据获取、操作等
+ - 按照资源名小写字母加连字符命名
+ - 目录分为两级:例如 `nova/instances.js`, `cinder/volume.js`
+- `src/utils`目录:
+ - 公共函数(时间处理、正则、cookie、localStorage、......)
+ - 对应的单元测试,以 test.js 或 spec.js 结尾
+- `src/styles`目录: 基础样式、公用样式、样式变量等
+- `src/pages`目录:
+ - 按照页面层级结构递进(按照:菜单项--二级菜单)
+ - 所有目录命名均为小写加连字符命名, 目录包含两个文件夹 `containers` 和 `routers`, 一个文件 `App.js`
+ - `containers`下存放二级目录对应的页面
+ - `routes`用于配置路由
+
+# src/pages 目录介绍
+
+- 以一级、二级菜单划分目录,一级菜单列在`src/pages`下,其对应的二级菜单页面位于`src/pages/xxx/containers`下,以“计算-云主机”为例,“计算”对应于`src/pages/compute`目录,“云主机”对应于`src/pages/compute/containers/Instance`目录
+- `src/pages/compute/containers/Instance/index.jsx`: 云主机列表页,继承于[BaseList 组件](3-1-BaseList-introduction.md)(带有 Tab
+ 的页面,继承 TabBaseList 组件即可)
+- `src/pages/compute/containers/Instance/Detail`目录
+ - 云主机详情页
+ - `index.jsx`继承于[BaseDetail 组件](3-3-BaseDetail-introduction.md)
+- `src/pages/compute/containers/Instance/actions`目录
+ - 云主机的操作
+ - `Lock.jsx` 锁定云主机,继承于[BaseConfirmAction](3-8-ConfirmAction-introduction.md)
+ - `AttachInterface.jsx` 继承于[BaseModalAction](3-7-ModalAction-introduction.md)
+ - `StepCreate/index.jsx`,继承于[BaseStepAction](3-9-StepAction-introduction.md)
+- `src/pages/compute/routes`目录:
+ - `index.js`,配置路由
+ - 约定以路由中是否含有“-admin”来判定是管理平台还是控制台
+
+# test 目录介绍
+
+[简体中文](/docs/zh/test/2-catalog-introduction.md) | [English](/docs/en/test/2-catalog-introduction.md)
+
+# 目录简介-图像版
+
+```
+.
+├── Gruntfile.js (用于收集i18n)
+├── LICENSE
+├── Makefile
+├── README.md
+├── config
+│ ├── theme.js
+│ ├── webpack.common.js
+│ ├── webpack.dev.js (开发时使用的webpack配置)
+│ ├── webpack.e2e.js (e2e测试时使用的webpack配置,能生成用于检测覆盖率的包)
+│ └── webpack.prod.js (生成环境使用的webpack打包配置)
+├── cypress.json (e2e的配置)
+├── docker
+│ ├── dev.dockerfile
+│ ├── nginx.conf
+│ ├── prod.dockerfile
+│ └── test.dockerfile
+├── docs (文档)
+├── jest.config.js (单元测试配置)
+├── jsconfig.json
+├── package.json
+├── src
+│ ├── api (api汇总,暂未使用)
+│ ├── asset
+│ │ ├── image (图片放置位置)
+│ │ └── template
+│ │ └── index.html
+│ ├── components (公用组件)
+│ ├── containers
+│ │ ├── Action
+│ │ │ ├── ConfirmAction (确认型的action基类)
+│ │ │ ├── FormAction (单页的action基类)
+│ │ │ ├── ModalAction (弹窗型的action基类)
+│ │ │ ├── StepAction (分多步的单页action,例如:创建云主机)
+│ │ │ └── index.jsx
+│ │ ├── BaseDetail (带有详情信息的详情页基类)
+│ │ ├── List (列表页的基类,例如:云主机)
+│ │ ├── TabDetail (带有tab切换的详情页的基类,例如:云主机详情)
+│ │ └── TabList (带有tab切换的列表页)
+│ ├── core
+│ │ ├── App.jsx
+│ │ ├── i18n.js
+│ │ ├── index.jsx (入口)
+│ │ └── routes.js (按模块的路由配置)
+│ ├── layouts
+│ │ ├── Base (登录后使用的布局)
+│ │ ├── Blank (空白布局)
+│ │ ├── User (登录使用的布局)
+│ │ ├── admin-menu.jsx (管理平台使用的菜单配置)
+│ │ └── menu.jsx (控制台使用的菜单配置)
+│ ├── locales (翻译)
+│ │ ├── en.json
+│ │ ├── index.js
+│ │ └── zh.json
+│ ├── pages (页面-目录结构按照:菜单项--二级菜单 分配,其中二级菜单的页面放在containers文件夹下)
+│ │ ├── base
+│ │ │ ├── App.jsx
+│ │ │ ├── containers
+│ │ │ │ ├── 404 (404页面)
+│ │ │ │ │ └── index.jsx
+│ │ │ │ ├── AdminOverview (管理平台首页)
+│ │ │ │ │ ├── components
+│ │ │ │ │ │ ├── ComputeService.jsx
+│ │ │ │ │ │ ├── NetworkService.jsx
+│ │ │ │ │ │ ├── PlatformInfo.jsx
+│ │ │ │ │ │ ├── ResourceOverview.jsx
+│ │ │ │ │ │ └── VirtualResource.jsx
+│ │ │ │ │ ├── index.jsx
+│ │ │ │ │ └── style.less
+│ │ │ │ └── Overview (控制台首页)
+│ │ │ │ ├── components
+│ │ │ │ │ ├── ProjectInfo.jsx
+│ │ │ │ │ ├── QuotaOverview.jsx
+│ │ │ │ │ └── ResourceStatistic.jsx
+│ │ │ │ ├── index.jsx
+│ │ │ │ └── style.less
+│ │ │ └── routes (路由配置)
+│ │ │ └── index.js
+│ │ ├── compute
+│ │ │ ├── App.jsx
+│ │ │ ├── containers
+│ │ │ │ ├── BareMetalNode (裸机配置)
+│ │ │ │ ├── Flavor (云主机类型)
+│ │ │ │ ├── HostAggregate (主机集合)
+│ │ │ │ │ ├── Aggregate (主机集合)
+│ │ │ │ │ ├── AvailabilityZone (可用域)
+│ │ │ │ │ └── index.jsx
+│ │ │ │ ├── Hypervisors (虚拟机管理器)
+│ │ │ │ │ ├── ComputeHost (计算节点)
+│ │ │ │ │ ├── Hypervisor (虚拟机管理器)
+│ │ │ │ │ └── index.jsx
+│ │ │ │ ├── Image (镜像)
+│ │ │ │ ├── Instance (云主机)
+│ │ │ │ │ ├── Detail (详情页)
+│ │ │ │ │ │ ├── BaseDetail (基础信息)
+│ │ │ │ │ │ ├── SecurityGroup (安全组)
+│ │ │ │ │ │ └── index.jsx
+│ │ │ │ │ ├── actions (操作)
+│ │ │ │ │ │ ├── AssociateFip.jsx (绑定浮动IP)
+│ │ │ │ │ │ ├── AttachInterface.jsx (挂载网卡)
+│ │ │ │ │ │ ├── AttachIsoVolume.jsx (挂载ISO光盘)
+│ │ │ │ │ │ ├── AttachVolume.jsx (挂载云硬盘)
+│ │ │ │ │ │ ├── ChangePassword.jsx (修改密码)
+│ │ │ │ │ │ ├── Console.jsx (控制台)
+│ │ │ │ │ │ ├── CreateImage.jsx (创建镜像)
+│ │ │ │ │ │ ├── CreateIronic (创建裸机-分步型Form)
+│ │ │ │ │ │ │ ├── BaseStep
+│ │ │ │ │ │ │ │ └── index.jsx
+│ │ │ │ │ │ │ ├── ConfirmStep
+│ │ │ │ │ │ │ │ └── index.jsx
+│ │ │ │ │ │ │ ├── NetworkStep
+│ │ │ │ │ │ │ │ └── index.jsx
+│ │ │ │ │ │ │ ├── SystemStep
+│ │ │ │ │ │ │ │ └── index.jsx
+│ │ │ │ │ │ │ ├── index.jsx
+│ │ │ │ │ │ │ └── index.less
+│ │ │ │ │ │ ├── CreateSnapshot.jsx (创建快照)
+│ │ │ │ │ │ ├── Delete.jsx (删除云主机)
+│ │ │ │ │ │ ├── DeleteIronic.jsx (删除裸机实例)
+│ │ │ │ │ │ ├── DetachInterface.jsx (卸载网卡)
+│ │ │ │ │ │ ├── DetachIsoVolume.jsx (卸载ISO镜像)
+│ │ │ │ │ │ ├── DetachVolume.jsx (卸载云硬盘)
+│ │ │ │ │ │ ├── DisassociateFip.jsx (解绑浮动IP)
+│ │ │ │ │ │ ├── Edit.jsx (编辑云主机)
+│ │ │ │ │ │ ├── ExtendRootVolume.jsx (扩容根磁盘)
+│ │ │ │ │ │ ├── LiveMigrate.jsx (热迁移)
+│ │ │ │ │ │ ├── Lock.jsx (锁定云主机)
+│ │ │ │ │ │ ├── ManageSecurityGroup.jsx (管理安全组)
+│ │ │ │ │ │ ├── Migrate.jsx (迁移)
+│ │ │ │ │ │ ├── Pause.jsx (暂停云主机)
+│ │ │ │ │ │ ├── Reboot.jsx (重启云主机)
+│ │ │ │ │ │ ├── Rebuild.jsx (重建云主机)
+│ │ │ │ │ │ ├── RebuildSelect.jsx (选镜像重建云主机)
+│ │ │ │ │ │ ├── Resize.jsx (修改配置)
+│ │ │ │ │ │ ├── ResizeOnline.jsx (在线修改配置)
+│ │ │ │ │ │ ├── Resume.jsx (恢复云主机)
+│ │ │ │ │ │ ├── Shelve.jsx (归档云主机)
+│ │ │ │ │ │ ├── SoftDelete.jsx (软删除云主机)
+│ │ │ │ │ │ ├── SoftReboot.jsx (软重启云主机)
+│ │ │ │ │ │ ├── Start.jsx (启动云主机)
+│ │ │ │ │ │ ├── StepCreate (创建云主机-分步创建)
+│ │ │ │ │ │ │ ├── BaseStep
+│ │ │ │ │ │ │ │ └── index.jsx
+│ │ │ │ │ │ │ ├── ConfirmStep
+│ │ │ │ │ │ │ │ └── index.jsx
+│ │ │ │ │ │ │ ├── NetworkStep
+│ │ │ │ │ │ │ │ └── index.jsx
+│ │ │ │ │ │ │ ├── SystemStep
+│ │ │ │ │ │ │ │ └── index.jsx
+│ │ │ │ │ │ │ ├── index.jsx
+│ │ │ │ │ │ │ └── index.less
+│ │ │ │ │ │ ├── Stop.jsx (关闭云主机)
+│ │ │ │ │ │ ├── Suspend.jsx (挂起云主机)
+│ │ │ │ │ │ ├── Unlock.jsx (解锁云主机)
+│ │ │ │ │ │ ├── Unpause.jsx (恢复暂停的云主机)
+│ │ │ │ │ │ ├── Unshelve.jsx (恢复归档的云主机)
+│ │ │ │ │ │ ├── index.jsx
+│ │ │ │ │ │ └── index.less
+│ │ │ │ │ ├── components (组件)
+│ │ │ │ │ │ ├── FlavorSelectTable.jsx
+│ │ │ │ │ │ └── index.less
+│ │ │ │ │ ├── index.jsx
+│ │ │ │ │ └── index.less
+│ │ │ │ ├── Keypair (密钥)
+│ │ │ │ └── ServerGroup (云主机组)
+│ │ │ └── routes (计算菜单下的路由配置)
+│ │ │ └── index.js
+│ │ ├── configuration (平台配置)
+│ │ │ ├── App.jsx
+│ │ │ ├── containers
+│ │ │ │ ├── Metadata (元数据定义)
+│ │ │ │ ├── Setting (系统配置)
+│ │ │ │ └── SystemInfo (系统信息)
+│ │ │ └── routes (平台配置菜单下的路由配置)
+│ │ │ └── index.js
+│ │ ├── heat (资源编排)
+│ │ │ ├── App.jsx
+│ │ │ ├── containers
+│ │ │ │ └── Stack (堆栈)
+│ │ │ └── routes (资源编排菜单下的路由配置)
+│ │ │ └── index.js
+│ │ ├── identity (身份管理)
+│ │ │ ├── App.jsx
+│ │ │ ├── containers
+│ │ │ │ ├── Domain (域)
+│ │ │ │ ├── Project (项目)
+│ │ │ │ ├── Role (角色)
+│ │ │ │ ├── User (用户)
+│ │ │ │ └── UserGroup (用户组)
+│ │ │ └── routes (路由配置)
+│ │ │ └── index.js
+│ │ ├── management (运维管理)
+│ │ │ ├── App.jsx
+│ │ │ ├── containers
+│ │ │ │ └── RecycleBin (回收站)
+│ │ │ └── routes (路由配置)
+│ │ │ └── index.js
+│ │ ├── network (网络)
+│ │ │ ├── App.jsx
+│ │ │ ├── containers
+│ │ │ │ ├── FloatingIp (浮动IP)
+│ │ │ │ ├── LoadBalancers (负载均衡)
+│ │ │ │ ├── Network (网络)
+│ │ │ │ ├── QoSPolicy (Qos策略)
+│ │ │ │ ├── Router (路由器)
+│ │ │ │ ├── SecurityGroup (安全组)
+│ │ │ │ ├── Topology (网络拓扑)
+│ │ │ │ ├── VPN (VPN)
+│ │ │ │ └── VirtualAdapter (虚拟网卡)
+│ │ │ └── routes (路由配置)
+│ │ │ └── index.js
+│ │ ├── storage (存储)
+│ │ │ ├── App.jsx
+│ │ │ ├── containers
+│ │ │ │ ├── Backup (备份)
+│ │ │ │ ├── Snapshot (云硬盘快照)
+│ │ │ │ ├── Storage (存储后端)
+│ │ │ │ ├── Volume (云硬盘)
+│ │ │ │ └── VolumeType (云硬盘类型)
+│ │ │ │ ├── QosSpec (QoS)
+│ │ │ │ ├── VolumeType (云硬盘类型)
+│ │ │ │ └── index.jsx
+│ │ │ └── routes ()
+│ │ │ └── index.js
+│ │ └── user (登录页面)
+│ │ ├── App.jsx
+│ │ ├── containers
+│ │ │ ├── ChangePassword (修改密码--根据系统配置)
+│ │ │ │ ├── index.jsx
+│ │ │ │ └── index.less
+│ │ │ └── Login (登录)
+│ │ │ ├── index.jsx
+│ │ │ └── index.less
+│ │ └── routes (路由配置)
+│ │ └── index.js
+│ ├── resources (存放各资源的自身使用的公用函数,状态等)
+│ ├── stores (数据处理,按资源类型划分文件夹)
+│ │ ├── base-list.js (列表数据的基类)
+│ │ ├── base.js (数据操作的基类)
+│ │ ├── cinder
+│ │ ├── glance
+│ │ ├── heat
+│ │ ├── ironic
+│ │ ├── keystone
+│ │ ├── neutron
+│ │ ├── nova
+│ │ ├── octavia
+│ │ ├── overview-admin.js
+│ │ ├── project.js
+│ │ ├── root.js
+│ │ └── skyline
+│ ├── styles (公用样式)
+│ │ ├── base.less
+│ │ ├── main.less
+│ │ ├── reset.less
+│ │ └── variables.less
+│ └── utils (基础函数)
+│ ├── RouterConfig.jsx
+│ ├── constants.js
+│ ├── cookie.js
+│ ├── file.js
+│ ├── file.spec.js
+│ ├── index.js
+│ ├── index.test.js (单元测试)
+│ ├── local-storage.js
+│ ├── local-storage.spec.js (单元测试)
+│ ├── request.js
+│ ├── table.jsx
+│ ├── time.js
+│ ├── time.spec.js
+│ ├── translate.js
+│ ├── translate.spec.js
+│ ├── validate.js
+│ ├── yaml.js
+│ └── yaml.spec.js
+├── test
+│ ├── e2e (E2E测试)
+│ └── unit (单元测试)
+├── tools
+│ └── git_config
+│ └── commit_message.txt
+└── yarn.lock
+```
diff --git a/docs/zh/develop/3-0-how-to-develop.md b/docs/zh/develop/3-0-how-to-develop.md
new file mode 100644
index 00000000..f9649513
--- /dev/null
+++ b/docs/zh/develop/3-0-how-to-develop.md
@@ -0,0 +1,114 @@
+简体中文 | [English](/docs/en/develop/3-0-how-to-develop.md)
+
+# 开发一个新的资源列表页
+
+- 步骤 1:确认代码位置及目录结构
+ - 按照预想的在菜单项中的位置,放置在 Containers 下
+ - 以云主机为例,对应的菜单项为`计算-云主机`,那么创建文件夹`src/pages/compute/containers/Instance`,创建文件`src/pages/compute/containers/Instance/index.jsx`
+- 步骤 2:编写 Store 代码
+ - 参考[3-5-BaseStore-introduction](3-5-BaseStore-introduction.md),复写相应的函数
+- 步骤 3:编写列表页代码
+ - 参考[3-1-BaseList-introduction](3-1-BaseList-introduction.md),复写相应的函数
+- 步骤 4:配置路由
+ - 参考[3-13-Route-introduction](3-13-Route-introduction.md)
+ - 在步骤 1 中的父级目录的`routes/index.js`文件中,配置路由
+ - 如果是全新的模块,还需要在`src/pages/storage/routes/index.js`中导入
+- 步骤 5:配置菜单
+ - 参考[3-12-Menu-introduction](3-12-Menu-introduction.md)
+ - 配置控制台的菜单项,在`src/layouts/menu.jsx`中配置
+ - 配置管理平台的菜单项,在`src/layouts/admin-menu.jsx`中配置
+- 步骤 6:国际化
+ - 参考[3-14-I18n-introduction](3-14-I18n-introduction.md),完成相应翻译
+- 如果,产品需求的列表页面是含有`Tab`的页面,则可参考[3-2-BaseTabList-introduction](3-2-BaseTabList-introduction.md),通常`index.jsx`内配置`Tab`,可参考镜像页面代码`src/pages/compute/containers/Image/index.jsx`
+
+# 开发一个新的资源详情页
+
+- 步骤 1:确认代码位置及目录结构
+ - 按照预想的在菜单项中的位置,放置在 Containers 下
+ - 以云主机为例,对应的菜单项为`计算-云主机`,创建文件`src/pages/compute/containers/Instance/Detail/index.jsx`,`src/pages/compute/containers/Instance/Detail/BaseDetail.jsx`
+- 步骤 2:编写 Store 代码
+ - 参考[3-5-BaseStore-introduction](3-5-BaseStore-introduction.md),复写相应的函数
+- 步骤 3:编写详情页代码
+ - 参考[3-3-BaseDetail-introduction](3-3-BaseDetail-introduction.md),复写相应的函数
+- 步骤 4:编写详情页-详情 Tab 代码
+ - 参考[3-4-BaseDetailInfo-introduction](3-4-BaseDetailInfo-introduction.md),复写相应的函数
+- 步骤 5:配置路由
+ - 参考[3-13-Route-introduction](3-13-Route-introduction.md)
+ - 在步骤 1 中的父级目录的`routes/index.js`文件中,配置路由
+ - 如果是全新的模块,还需要在`src/pages/storage/routes/index.js`中导入
+- 步骤 6:配置菜单
+ - 参考[3-12-Menu-introduction](3-12-Menu-introduction.md)
+ - 配置控制台的菜单项,在`src/layouts/menu.jsx`中配置
+ - 配置管理平台的菜单项,在`src/layouts/admin-menu.jsx`中配置
+- 步骤 7:国际化
+ - 参考[3-14-I18n-introduction](3-14-I18n-introduction.md),完成相应翻译
+
+# 开发一个新的操作
+
+## 开发一个页面级的操作
+
+- 步骤 1:确认代码位置及目录结构
+ - 按照预想的在菜单项中的位置,放置在 Containers 下
+ - 以云硬盘为例,对应的菜单项为`存储-云硬盘-云硬盘创建`,创建文件`src/pages/storage/containers/Volume/actions/Create/index.jsx`
+- 步骤 2:编写 Store 代码
+ - 参考[3-5-BaseStore-introduction](3-5-BaseStore-introduction.md),复写或新增相应的函数
+- 步骤 3:编写 FormAction 代码
+ - 参考[3-6-FormAction-introduction](3-6-FormAction-introduction.md),复写相应的函数
+- 步骤 4:配置 Action
+ - 参考[3-11-Action-introduction](3-11-Action-introduction.md),配置到相应为位置
+- 步骤 5:配置路由
+ - 参考[3-13-Route-introduction](3-13-Route-introduction.md),配置对应的路由
+- 步骤 6:配置菜单
+ - 参考[3-12-Menu-introduction](3-12-Menu-introduction.md)
+ - 配置控制台的菜单项,在`src/layouts/menu.jsx`中配置
+ - 配置管理平台的菜单项,在`src/layouts/admin-menu.jsx`中配置
+- 步骤 7:国际化
+ - 参考[3-14-I18n-introduction](3-14-I18n-introduction.md),完成相应翻译
+
+## 开发一个确认型的操作
+
+- 步骤 1:确认代码位置及目录结构
+ - 按照预想的在菜单项中的位置,放置在 Containers 下
+ - 以云硬盘为例,对应的菜单项为`存储-云硬盘-删除云硬盘`,创建文件`src/pages/storage/containers/Volume/actions/Delete.jsx`
+- 步骤 2:编写 Store 代码
+ - 参考[3-5-BaseStore-introduction](3-5-BaseStore-introduction.md),复写或新增相应的函数
+- 步骤 3:编写 ConfirmAction 代码
+ - 参考[3-8-ConfirmAction-introduction](3-8-ConfirmAction-introduction.md),复写相应的函数
+- 步骤 4:配置 Action
+ - 参考[3-11-Action-introduction](3-11-Action-introduction.md),配置到相应为位置
+- 步骤 5:国际化
+ - 参考[3-14-I18n-introduction](3-14-I18n-introduction.md),完成相应翻译
+
+## 开发一个弹窗型的操作
+
+- 步骤 1:确认代码位置及目录结构
+ - 按照预想的在菜单项中的位置,放置在 Containers 下
+ - 以云硬盘为例,对应的菜单项为`存储-云硬盘-编辑`,创建文件`src/pages/storage/containers/Volume/actions/Edit.jsx`
+- 步骤 2:编写 Store 代码
+ - 参考[3-5-BaseStore-introduction](3-5-BaseStore-introduction.md),复写或新增相应的函数
+- 步骤 3:编写 ModalAction 代码
+ - 参考[3-7-ModalAction-introduction](3-7-ModalAction-introduction.md),复写相应的函数
+- 步骤 4:配置 Action
+ - 参考[3-11-Action-introduction](3-11-Action-introduction.md),配置到相应为位置
+- 步骤 5:国际化
+ - 参考[3-14-I18n-introduction](3-14-I18n-introduction.md),完成相应翻译
+
+## 开发一个分步骤的页面级的操作
+
+- 步骤 1:确认代码位置及目录结构
+ - 按照预想的在菜单项中的位置,放置在 Containers 下
+ - 以云硬盘为例,对应的菜单项为`计算-云主机-创建`,创建文件`src/pages/compute/containers/Instance/actions/StepCreate/index.jsx`
+- 步骤 2:编写 Store 代码
+ - 参考[3-5-BaseStore-introduction](3-5-BaseStore-introduction.md),复写或新增相应的函数
+- 步骤 3:编写 StepAction 代码
+ - 参考[3-9-StepAction-introduction](3-9-StepAction-introduction.md),复写相应的函数
+- 步骤 4:配置 Action
+ - 参考[3-11-Action-introduction](3-11-Action-introduction.md),配置到相应为位置
+- 步骤 5:配置路由
+ - 参考[3-13-Route-introduction](3-13-Route-introduction.md),配置对应的路由
+- 步骤 6:配置菜单
+ - 参考[3-12-Menu-introduction](3-12-Menu-introduction.md)
+ - 配置控制台的菜单项,在`src/layouts/menu.jsx`中配置
+ - 配置管理平台的菜单项,在`src/layouts/admin-menu.jsx`中配置
+- 步骤 7:国际化
+ - 参考[3-14-I18n-introduction](3-14-I18n-introduction.md),完成相应翻译
diff --git a/docs/zh/develop/3-1-BaseList-introduction.md b/docs/zh/develop/3-1-BaseList-introduction.md
new file mode 100644
index 00000000..09b793bd
--- /dev/null
+++ b/docs/zh/develop/3-1-BaseList-introduction.md
@@ -0,0 +1,563 @@
+简体中文 | [English](/docs/en/develop/3-1-BaseList-introduction.md)
+
+# 用途
+
+- 各资源列表页的基类
+
+ ![列表页](/docs/zh/develop/images/list/volumes.png)
+
+- 支持数据分页
+
+ ![列表页分页](/docs/zh/develop/images/list/pagination.png)
+
+- 支持搜索
+
+ ![列表页搜索](/docs/zh/develop/images/list/search.png)
+
+- 支持手动刷新数据
+
+ ![列表页刷新](/docs/zh/develop/images/list/fresh.png)
+
+- 支持数据下载
+
+ ![列表页下载](/docs/zh/develop/images/list/download.png)
+
+- 支持批量操作
+
+ ![列表页批量](/docs/zh/develop/images/list/batch.png)
+
+- 具有自动刷新数据的功能(每隔 60 秒自动刷新列表数据,用户无操作的情况下,30 分钟后不再自动刷新,可暂停自动刷新功能)
+
+ ![列表页自动刷新](/docs/zh/develop/images/list/stop-auto-refresh.png)
+
+- 可配置列表表头
+
+ ![列表页自动刷新](/docs/zh/develop/images/list/hide.png)
+
+- 各资源列表页通过复写函数即可完成
+
+# BaseList 代码文件
+
+- `src/containers/List/index.jsx`
+
+# BaseList 属性与函数定义介绍
+
+- 资源列表继承于 BaseList 组件
+- 只需要复写部分函数即可完成页面的开发
+- 属性与函数分为以下四种,
+ - 通常需要复写的属性与函数,主要包含:
+ - 页面的权限
+ - 页面的资源名称
+ - 表格的列的配置
+ - 表格的搜索项
+ - 表格的操作项等
+ - 表格对应的`store`
+ - 按需复写的函数与属性,主要包含:
+ - 资源数据分页使用前端分页还是后端分页
+ - 资源数据排序使用前端排序还是后端排序
+ - 无需复写的函数与属性,主要包含:
+ - 当前页是否是管理平台页面
+ - 当前页是否是详情页中的资源列表
+ - 基类中的基础函数,主要包含:
+ - 渲染页面
+ - 处理下载
+ - 处理自动刷新
+ - 隐藏/展示某些表格列
+ - 处理搜索
+ - 处理分页信息变动后的数据请求与展示
+ - 更详细与全面的介绍见下
+
+## 通常需要复写的属性与函数
+
+- `policy`:
+ - 必须复写该函数
+ - 页面对应的权限,如果权限验证不通过,则无法请求数据。
+ - 以云硬盘`src/pages/storage/containers/Volume/index.jsx`为例
+
+ ```javascript
+ get policy() {
+ return 'volume:get_all';
+ }
+ ```
+
+- `name`
+ - 必须复写该函数
+ - 页面资源对应的名称。
+ - 以云硬盘`src/pages/storage/containers/Volume/index.jsx`为例
+
+ ```javascript
+ get name() {
+ return t('volumes');
+ }
+ ```
+
+- `actionConfigs`
+ - 配置资源的各种操作
+ - 主按钮操作,如:创建
+ - 批量操作
+ - 每一行数据的操作
+ - 配置定义在资源的 actions 目录下
+ - 以密钥`src/pages/compute/containers/Keypair/index.jsx`为例
+
+ ```javascript
+ import actionConfigs from './actions';
+ get actionConfigs() {
+ return actionConfigs;
+ }
+ ```
+
+- `searchFilters`
+ - 配置资源的搜索项
+ - 支持基于字符串的搜索
+ - 支持选择的搜索,如:基于状态的搜索
+ - 支持多种搜索条件均需满足的搜索
+ - 返回配置的数组,每个配置代表一个搜索条件
+ - 每个配置需要满足一下条件:
+ - `label`,必须项,搜索的标题
+ - `name`,必须项,该搜索项对应的参数`Key`
+ - `options`,可选项
+ - 如果不设置`options`属性,表示,该搜索是基于输入字符串的搜索,如:对名称的搜索
+ - 如设置`options`属性,则在页面内需要从`options`中选择
+ - `options`的格式:
+ - `key`: 必须项,`option`对应的值
+ - `label`:必须项,`option`对应的文字,即页面上看到的内容
+ - 以云硬盘`src/pages/storage/containers/Volume/index.jsx`为例
+
+ ```javascript
+ get searchFilters() {
+ return [
+ {
+ label: t('Name'),
+ name: 'name',
+ },
+ {
+ label: t('Status'),
+ name: 'status',
+ options: ['available', 'in-use', 'error'].map((it) => ({
+ key: it,
+ label: volumeStatus[it],
+ })),
+ },
+ {
+ label: t('Shared'),
+ name: 'multiattach',
+ options: yesNoOptions,
+ },];
+ }
+ ```
+
+- `getColumns`
+ - 返回列表表格的配置信息列表
+ - 每个配置项的设置:
+ - `title`,必须项,表头的标题
+ - `dataIndex`,必须项,对应的后端数据的 key 值
+ - `hidden`,可选项,该列是否可隐藏,默认值为`false`
+ - `sorter`,该列是否可排序,默认可排序
+ - `stringify`,可选项,下载到`csv`中时,该列中数据显示的内容,因为有些列有额外的样式或是 UI 处理,会导致对该列的转字符串的结果出现问题,此时需要编写该函数
+ - `render`,可选项,默认是基于`dataIndex`来展示内容,使用该属性,可基于`render`的结果渲染表格内容
+ - `valueRender`,可选项,使用已有的函数自动处理数据
+ - `sinceTime`,处理时间,显示成"XX 小时前"
+ - `keepTime`,显示剩余时间
+ - `yesNo`,处理`Boolean`值,显示成“是”、“否”
+ - `GBValue`,处理大小,显示成"XXXGB"
+ - `noValue`,没有值时,显示成“-”
+ - `bytes`,处理大小
+ - `uppercase`,大写
+ - `formatSize`,处理大小,显示如“2.32 GB”,“56.68 MB”
+ - `toLocalTime`,处理时间,显示如“2021-06-17 04:13:07”
+ - `toLocalTimeMoment`,处理时间,显示如“2021-06-17 04:13:07”
+ - `linkPrefix`,可选项,当`dataIndex=name`时,`linkPrefix`属性用于处理名称对应的链接的前缀
+ - 以镜像`src/pages/compute/containers/Image/Image.jsx`为例
+ - 表格包含的列:ID/名称、项目 ID/名称(管理平台中展示)、描述、使用类型、类型、状态、可见性、硬盘格式、容量、创建于
+
+ ```javascript
+ getColumns = () => [
+ {
+ title: t('ID/Name'),
+ dataIndex: 'name',
+ linkPrefix: `/compute/${this.getUrl('image')}/detail`,
+ },
+ {
+ title: t('Project ID/Name'),
+ dataIndex: 'project_name',
+ hidden: !this.isAdminPage && this.tab !== 'all',
+ sorter: false,
+ },
+ {
+ title: t('Description'),
+ dataIndex: 'description',
+ isHideable: true,
+ sorter: false,
+ },
+ {
+ title: t('Use Type'),
+ dataIndex: 'usage_type',
+ isHideable: true,
+ render: (value) => imageUsage[value] || '-',
+ sorter: false,
+ },
+ {
+ title: t('Type'),
+ dataIndex: 'os_distro',
+ isHideable: true,
+ render: (value) => ,
+ width: 80,
+ sorter: false,
+ },
+ {
+ title: t('Status'),
+ dataIndex: 'status',
+ render: (value) => imageStatus[value] || '-',
+ },
+ {
+ title: t('Visibility'),
+ dataIndex: 'visibility',
+ render: (value) => imageVisibility[value] || '-',
+ sorter: false,
+ },
+ {
+ title: t('Disk Format'),
+ dataIndex: 'disk_format',
+ isHideable: true,
+ render: (value) => imageFormats[value] || '-',
+ },
+ {
+ title: t('Size'),
+ dataIndex: 'size',
+ isHideable: true,
+ valueRender: 'formatSize',
+ },
+ {
+ title: t('Created At'),
+ dataIndex: 'created_at',
+ isHideable: true,
+ valueRender: 'sinceTime',
+ },
+ ];
+ ```
+
+- `init`
+ - 配置 Store 的函数,在这个函数中配置用于处理数据请求的 Store,以及用于下载数据的 Store
+ - 通常使用的是单例的 Store,但是对于某些详情页下的列表页,使用`new XXXStore()`
+ - `init`中可配置`this.store`与`this.downloadStore`
+ - `this.store`用于处理列表数据
+ - `this.downloadStore`用于处理下载数据
+ - 如果使用前端页面,只配置`this.store`即可,因为是一次性获取所有的数据,下载的数据等于列表中的数据,即这时,`this.downloadStore = this.store`
+ - 以项目`src/pages/identity/containers/Project/index.jsx`为例
+
+ ```javascript
+ init() {
+ this.store = globalProjectStore;
+ }
+ ```
+
+ - 如果使用后端分页,需要分别配置`this.store`与`this.downloadStore`
+ - 以路由器`src/pages/network/containers/Router/index.jsx`为例
+
+ ```javascript
+ init() {
+ this.store = new RouterStore();
+ this.downloadStore = new RouterStore();
+ }
+ ```
+
+## 按需复写的属性与函数
+
+- `alsoRefreshDetail`
+ - 详情页中的列表数据刷新时,是否需要同步刷新详情数据
+ - 默认同步刷新,如不需要同步刷新,复写该函数
+
+ ```javascript
+ get alsoRefreshDetail() {
+ return false;
+ }
+ ```
+
+- `list`
+ - 页面对象的 store 中的数据
+ - 默认值是`this.store.list`
+- `rowKey`
+ - 列表数据的唯一标识的 Key
+ - 默认值是`id`
+ - 以密钥 Keypair `src/pages/compute/containers/Keypair/index.jsx`为例
+
+ ```javascript
+ get rowKey() {
+ return 'name';
+ }
+ ```
+
+- `hasTab`
+ - 列表页是否是 Tab 下的列表页
+ - 默认值为`false`
+ - 会根据改值调整表格的高度
+ - 以`src/pages/configuration/containers/SystemInfo/Catalog.jsx`为例
+
+ ```javascript
+ get hasTab() {
+ return true;
+ }
+ ```
+
+ ![列表页Tab](/docs/zh/develop/images/list/tab-service.png)
+
+- `hideCustom`
+ - 是否显示表头配置图标
+ - 默认值是`true`
+ - 以`src/pages/configuration/containers/Setting/index.jsx`为例
+
+ ```javascript
+ get hideCustom() {
+ return 'name';
+ }
+ ```
+
+- `hideSearch`
+ - 是否显示搜索框
+ - 默认显示
+ - 以资源编排-堆栈-详情页-日志`src/pages/heat/containers/Stack/Detail/Event.jsx`为例
+
+ ```javascript
+ get hideSearch() {
+ return true;
+ }
+ ```
+
+- `hideRefresh`
+ - 是否显示自动刷新按钮
+ - 默认显示
+ - 如不显示,则列表不具有自动刷新数据功能
+- `hideDownload`
+ - 是否展示下载按钮
+ - 默认显示
+- `checkEndpoint`
+ - 是否需要检测 endpoint
+ - 默认不需要
+ - 某些服务可能未部署,需要二次验证,一旦检测未部署,是显示“未开放”样式页面
+ - 以 VPN`src/pages/network/containers/VPN/index.jsx`为例
+
+ ```javascript
+ get checkEndpoint() {
+ return true;
+ }
+ ```
+
+- `endpoint`
+ - 当`checkEndpoint`为`true`时使用
+ - 以 VPN`src/pages/network/containers/VPN/index.jsx`为例
+
+ ```javascript
+ get endpoint() {
+ return vpnEndpoint();
+ }
+ ```
+
+- `isFilterByBackend`
+ - 是否由后端分页
+ - 默认值是`false`,即使用前端分页
+ - 使用前端分页时,是一次性从后端获取全部数据,然后按页面内的页码、单页数量展示数据
+ - 使用后端分页时,以页面、单页数量向后端请求相应数量的数据
+ - 以路由器`src/pages/network/containers/Router/index.jsx`为例
+
+ ```javascript
+ get isFilterByBackend() {
+ return true;
+ }
+ ```
+
+- `isSortByBackend`
+ - 是否由后端排序
+ - 默认值是`false`,即使用前端排序
+ - 使用前端排序时,基于列表内的数据大小排序(可自定义排序函数)
+ - 如果使用前端分页+前端排序,那么能基于所有数据排序
+ - 如果使用后端分页+前端排序,只能基于当前页的数据排序
+ - 使用后端分页时,按列表内设置的排序项、排序方向向后端请求数据
+ - 以路由器`src/pages/network/containers/Router/index.jsx`为例
+
+ ```javascript
+ get isSortByBackend() {
+ return true;
+ }
+ ```
+
+ - 当`isSortByBackend`设置为`true`时,通常需要重写相应`store`中的`updateParamsSortPage`函数
+ - 以`src/stores/neutron/router.js`为例
+
+ ```javascript
+ updateParamsSortPage = (params, sortKey, sortOrder) => {
+ if (sortKey && sortOrder) {
+ params.sort_key = sortKey;
+ params.sort_dir = sortOrder === 'descend' ? 'desc' : 'asc';
+ }
+ };
+ ```
+
+- `adminPageHasProjectFilter`
+ - 管理平台的搜索项中是否包含基于项目 ID 的搜索
+ - 默认值为`false`
+ - 以云主机`src/pages/compute/containers/Instance/index.jsx`为例
+
+ ```javascript
+ get adminPageHasProjectFilter() {
+ return true;
+ }
+ ```
+
+- `transitionStatusList`
+ - 数据处于过渡状态时对应的状态值列表
+ - 默认值为`[]`空列表
+ - 数据处于过渡状态时,页面的自动刷新会加快,变为 30 秒一次
+ - 默认值为`false`
+ - 以云硬盘`src/pages/storage/containers/Volume/index.jsx`为例
+
+ ```javascript
+ const volumeTransitionStatuses = [
+ 'creating',
+ 'extending',
+ 'downloading',
+ 'attaching',
+ 'detaching',
+ 'deleting',
+ 'backing-up',
+ 'restoring-backup',
+ 'awaiting-transfer',
+ 'uploading',
+ 'rollbacking',
+ 'retyping',
+ ];
+ get transitionStatusList() {
+ return volumeTransitionStatuses;
+ }
+ ```
+
+- `fetchDataByAllProjects`
+ - 管理平台请求数据时,是否带有`all_projects`参数
+ - 默认值为`true`
+ - 以云硬盘类型`src/pages/storage/containers/VolumeType/VolumeType/index.jsx`为例
+
+ ```javascript
+ get fetchDataByAllProjects() {
+ return false;
+ }
+ ```
+
+- `fetchDataByCurrentProject`
+ - 控制台请求数据时,是否带有`project_id`参数
+ - 默认值为`false`
+ - 以浮动 IP`src/pages/network/containers/FloatingIp/index.jsx`为例
+
+ ```javascript
+ get fetchDataByCurrentProject() {
+ return true;
+ }
+ ```
+
+- `defaultSortKey`
+ - 使用后端排序时,默认的排序 Key
+ - 以路由器`src/pages/network/containers/Router/index.jsx`为例
+
+ ```javascript
+ get defaultSortKey() {
+ return 'status';
+ }
+ ```
+
+- `clearListUnmount`
+ - 页面切换时,是否需要情况当前 store 内的 list 数据
+ - 一般情况,资源列表页使用的是`GlobalXXStore`,即单例的 Store,页面切换时,列表数据并不会清空,当回到该页面时,会先展示之前的数据,然后页面自动刷新获取新数据
+ - 默认值为`false`,页面切换时不清空数据
+- `ableAutoFresh`
+ - 是否自动刷新
+ - 默认值为`true`
+- `projectFilterKey`
+ - 请求时,project 对应的 key 值
+ - 默认值是`project_id`
+ - 以镜像`src/pages/compute/containers/Image/Image.jsx`为例
+
+ ```javascript
+ get projectFilterKey() {
+ return 'owner';
+ }
+ ```
+
+- `getCheckboxProps`
+ - 列表内的数据是否可选择,选中后可进行批量操作
+ - 默认都可选择
+ - 以云主机`src/pages/compute/containers/Instance/index.jsx`为例
+ - 裸机实例不可被选择
+
+ ```javascript
+ getCheckboxProps(record) {
+ return {
+ disabled: isIronicInstance(record),
+ name: record.name,
+ };
+ }
+ ```
+
+- `getData`
+ - 处理数据请求的函数
+ - 默认使用`store.fetchList`或`store.fetchListByPage`方法从服务端获取数据
+ - 不建议复写该函数
+- `fetchDataByPage`
+ - 采用后端分页时,处理数据请求的函数
+ - 默认使用`store.fetchListByPage`方法获取数据
+ - 不建议复写该函数
+- `fetchData`
+ - 采用前端分页时,处理数据请求的函数
+ - 默认使用`store.fetchList`方法获取数据
+ - 不建议复写该函数
+- `updateFetchParamsByPage`
+ - 采用后端分页时,在基类的基础上调整请求参数的函数
+ - 如果基类的默认参数无法满足请求时,建议通过复写该函数,并同步修改对应的`store`中的`listDidFetch`方法以完成数据请求
+- `updateFetchParams`
+ - 采用前端分页时,在基类的基础上调整请求参数的函数
+ - 如果基类的默认参数无法满足请求时,建议通过复写该函数,并同步修改对应的`store`中的`listDidFetch`方法以完成数据请求
+- `updateHints`
+ - 表格上放的提示语
+
+## 不需要复写的属性与函数
+
+- `isInDetailPage`
+ - 标识当前页面是否为详情页下的列表页
+- `location`
+ - 页面的路由信息
+- `isAdminPage`
+ - 当前页面是否是“管理平台”的页面
+- `hasAdminRole`
+ - 登录的用户角色是否具有管理员角色
+- `getUrl`
+ - 生成页面 Url 的函数
+ - 如:需要给列表页的关联资源提供跳转功能,使用该函数,可以在控制台跳转到控制台的相应地址,在管理平台跳转到管理平台的相应地址
+- `params`
+ - 路由带有的参数信息
+ - 一般用于生成页面请求 API 时的参数
+- `routing`
+ - 页面对应的路由信息
+- `isLoading`
+ - 当前页面是否在数据更新,更新时会显示 loading 样式
+- `endpointError`
+ - 判定 Endpoint 是否有效
+- `hintHeight`
+ - 页面内的提示的高度
+- `tableTopHeight`
+ - 表格上方占用的高度
+ - 基于提示、Tab 计算
+- `tableHeight`
+ - 表格的高度
+- `currentProjectId`
+ - 当前登录的用户所属的项目 ID
+- `defaultSortOrder`
+ - 使用后端排序时,默认的排序方向为降序`descend`
+- `itemInTransitionFunction`
+ - 判定是否有数据处于过渡状态,如果有数据处于过渡状态,则自动刷新数据的时间间隔由 60 秒变为 30 秒
+- `primaryActions`
+ - 主按钮操作列表
+- `batchActions`
+ - 批量操作列表
+- `itemActions`
+ - 每一行数据对应的操作列表
+
+## 基类中的基础函数
+
+- 建议查看代码理解,`src/containers/List/index.jsx`
diff --git a/docs/zh/develop/3-10-FormItem-introduction.md b/docs/zh/develop/3-10-FormItem-introduction.md
new file mode 100644
index 00000000..84a272f6
--- /dev/null
+++ b/docs/zh/develop/3-10-FormItem-introduction.md
@@ -0,0 +1,1047 @@
+简体中文 | [English](/docs/en/develop/3-10-FormItem-introduction.md)
+
+# 用途
+
+- 表单中每个表单项的配置
+- 一般只需要配置`type`等少量参数即可使用
+- `Form`组件会基于每个`formItem`的配置对输入的数值进行相应的验证
+- `Form`验证不通过将无法点击`确认`或`下一步`按钮
+
+# 如何使用
+
+- 每个表单项包含通用型配置
+ - `name`,表单项的`key`值,必须项,且唯一,表单项的值在验证通过后保存在`form.values[name]`中
+ - `label`,表单项左侧的标签
+ - `required`, 可选项,默认值为`false`,值为`true`时为必填项
+ - `hidden`, 当前表单项是否可隐藏,默认值为`false`
+ - `onChange`,当前表单项变更时触发的函数
+ - `extra`,表单想下方的说明文字
+ - 以创建网络`src/pages/network/containers/Network/actions/CreateNetwork.jsx`为例
+
+ ```javascript
+ {
+ name: 'mtu',
+ label: t('MTU'),
+ type: 'input-number',
+ min: 68,
+ max: 9000,
+ extra: t('Minimum value is 68 for IPv4, and 1280 for IPv6.'),
+ }
+ ```
+
+ ![extra](/docs/zh/develop/images/form/form-extra.png)
+
+ - `tip`,表单项左侧标签旁边的问号悬停时显示的内容
+ - 以创建云主机`src/pages/compute/containers/Instance/actions/StepCreate/BaseStep/index.jsx`为例
+
+ ```javascript
+ {
+ name: 'availableZone',
+ label: t('Available Zone'),
+ type: 'select',
+ placeholder: t('Please select'),
+ isWrappedValue: true,
+ required: true,
+ options: this.availableZones,
+ tip: t(
+ 'Availability zone refers to a physical area where power and network are independent of each other in the same area. In the same region, the availability zone and the availability zone can communicate with each other in the intranet, and the available zones can achieve fault isolation.'
+ ),
+ }
+ ```
+
+ ![tip](/docs/zh/develop/images/form/form-tip.png)
+
+ - `validator`,验证表单的数值是否符合要求
+ - 返回`Promise`
+ - 以裸机节点创建端口`src/pages/compute/containers/BareMetalNode/Detail/Port/actions/Create.jsx`为例
+
+ ```javascript
+ export const macAddressValidate = (rule, value) => {
+ if (isMacAddress(value.toUpperCase())) {
+ return Promise.resolve(true);
+ }
+ return Promise.reject(new Error(`${t('Invalid: ')}${macAddressMessage}`));
+ };
+ {
+ name: 'address',
+ label: t('MAC Address'),
+ required: true,
+ type: 'input',
+ validator: macAddressValidate,
+ }
+ ```
+
+ - `component`,直接使用`component`中的组件,而不是使用`type`配置的组件
+ - 以云主机修改配置`src/pages/compute/containers/Instance/actions/Resize.jsx`为例
+ - 直接展示云主机类型选择组件
+
+ ```javascript
+ {
+ name: 'newFlavor',
+ label: t('Flavor'),
+ component: (
+
+ ),
+ required: true,
+ wrapperCol: {
+ xs: {
+ span: 24,
+ },
+ sm: {
+ span: 18,
+ },
+ },
+ }
+ ```
+
+ - `labelCol`,调整表单项标题的布局,默认使用`Form`下定义的标签布局
+ - 以项目管理配额`src/pages/identity/containers/Project/actions/QuotaManager.jsx`为例
+
+ ```javascript
+ {
+ name: 'instances',
+ label: t('instance'),
+ type: 'input-number',
+ labelCol: { span: 12 },
+ colNum: 2,
+ validator: this.checkMin,
+ }
+ ```
+
+ ![labelCol](/docs/zh/develop/images/form/label-col.png)
+
+ - `wrapperCol`,调整表单项右侧的布局,默认使用`Form`下定义的布局
+ - 以云主机修改配置`src/pages/compute/containers/Instance/actions/Resize.jsx`为例
+ - 直接展示云主机类型选择组件
+
+ ```javascript
+ {
+ name: 'newFlavor',
+ label: t('Flavor'),
+ component: (
+
+ ),
+ required: true,
+ wrapperCol: {
+ xs: {
+ span: 24,
+ },
+ sm: {
+ span: 18,
+ },
+ },
+ }
+ ```
+
+ ![wrapperCol](/docs/zh/develop/images/form/wrapper-col.png)
+
+ - `style`,定义表单项的样式
+ - 以创建虚拟网卡`src/pages/network/containers/VirtualAdapter/actions/Create.jsx`为例
+
+ ```javascript
+ {
+ name: 'ipv6',
+ label: 'IPv6',
+ type: 'label',
+ style: { marginBottom: 24 },
+ content: (
+
+ {t('The selected VPC/ subnet does not have IPv6 enabled.')}{' '}
+
+ {t('To open')}
+ {' '}
+
+ ),
+ hidden: true,
+ }
+ ```
+
+ - `dependencies`,依赖项,数组,依赖项的数值变动后,会触发当前表单项的验证
+ - 以云主机更新密码`src/pages/compute/containers/Instance/actions/ChangePassword.jsx`为例
+ - 确认密码的验证,要依赖于密码的输入
+
+ ```javascript
+ {
+ name: 'confirmPassword',
+ label: t('Confirm Password'),
+ type: 'input-password',
+ dependencies: ['password'],
+ required: true,
+ otherRule: getPasswordOtherRule('confirmPassword', 'instance'),
+ },
+ ```
+
+ - `otherRule`,额外的验证规则
+
+- 每个表单根据自己的`type`有独属于自身的配置项,目前支持的`type`有
+ - `label`
+ - 展示内容使用
+ - `iconType`属性,可以显示资源对应的 icon
+
+ ```javascript
+ const iconTypeMap = {
+ instance: ,
+ router: ,
+ externalNetwork: ,
+ network: ,
+ firewall: ,
+ volume: ,
+ gateway: ,
+ user: ,
+ snapshot: ,
+ backup: ,
+ keypair: ,
+ image: ImageIcon,
+ aggregate: ,
+ metadata: ,
+ flavor: ,
+ host: ,
+ };
+ ```
+
+ - 以云主机挂载云硬盘`src/pages/compute/containers/Instance/actions/AttachVolume.jsx`为例
+
+ ```javascript
+ {
+ name: 'instance',
+ label: t('Instance'),
+ type: 'label',
+ iconType: 'instance',
+ },
+ ```
+
+ ![label](/docs/zh/develop/images/form/form-label.png)
+
+ - `content`属性,默认是基于`name`属性展示内容,如果具有`content`属性,则依照`content`展示内容
+ - `content`可以是字符串,也可以是 ReactNode
+ - 以虚拟网卡修改 QoS`src/pages/network/containers/VirtualAdapter/actions/ModifyQoS.jsx`为例
+
+ ```javascript
+ {
+ name: 'name',
+ label: t('Current QoS policy name'),
+ type: 'label',
+ content:
{qosPolicy.name || t('Not yet bound')}
,
+ hidden: !enableQosPolicy,
+ }
+ ```
+
+ - `input`
+ - 输入框
+ - 以编辑镜像`src/pages/compute/containers/Image/actions/Edit.jsx`为例
+ - 输入系统版本
+
+ ```javascript
+ {
+ name: 'os_version',
+ label: t('OS Version'),
+ type: 'input',
+ required: true,
+ },
+ ```
+
+ ![input](/docs/zh/develop/images/form/input.png)
+
+ - `select`
+ - 选择
+ - `options`,必须项,`option`数组,每个`option`需要具有如下属性
+ - `value`,值
+ - `label`,展示的文本
+ - 以创建云主机选择可用域`src/pages/compute/containers/Instance/actions/StepCreate/BaseStep/index.jsx`为例
+
+ ```javascript
+ get availableZones() {
+ return (globalAvailabilityZoneStore.list.data || [])
+ .filter((it) => it.zoneState.available)
+ .map((it) => ({
+ value: it.zoneName,
+ label: it.zoneName,
+ }));
+ }
+
+ {
+ name: 'availableZone',
+ label: t('Available Zone'),
+ type: 'select',
+ placeholder: t('Please select'),
+ isWrappedValue: true,
+ required: true,
+ options: this.availableZones,
+ tip: t(
+ 'Availability zone refers to a physical area where power and network are independent of each other in the same area. In the same region, the availability zone and the availability zone can communicate with each other in the intranet, and the available zones can achieve fault isolation.'
+ ),
+ },
+ ```
+
+ ![select](/docs/zh/develop/images/form/select.png)
+
+ - `isWrappedValue`,表示表单项的值中是否要包含`option`信息
+ - 默认值为`false`,值为选中的`option`中的`value`
+ - 如果设为`true`,值为选中的`option`
+ - `divider`
+ - 横线分隔符
+ - 以创建云主机`src/pages/compute/containers/Instance/actions/StepCreate/BaseStep/index.jsx`为例
+
+ ```javascript
+ {
+ type: 'divider',
+ }
+ ```
+
+ ![divider](/docs/zh/develop/images/form/form-divider.png)
+
+ - `radio`
+ - 单选
+ - `options`,必须项,`option`数组,每个`option`需要具有如下属性
+ - `value`,值
+ - `label`,展示的文本
+ - 以创建云主机选择登陆凭证类型`src/pages/compute/containers/Instance/actions/StepCreate/SystemStep/index.jsx`为例
+
+ ```javascript
+ get loginTypes() {
+ return [
+ {
+ label: t('Keypair'),
+ value: 'keypair',
+ disabled: this.isWindowsImage,
+ },
+ {
+ label: t('Password'),
+ value: 'password',
+ },
+ ];
+ }
+
+ {
+ name: 'loginType',
+ label: t('Login Type'),
+ type: 'radio',
+ options: this.loginTypes,
+ isWrappedValue: true,
+ },
+ ```
+
+ ![radio](/docs/zh/develop/images/form/radio.png)
+
+ - `isWrappedValue`,表示表单项的值中是否要包含`option`信息
+ - 默认值为`false`,值为选中的`option`中的`value`
+ - 如果设为`true`,值为选中的`option`
+ - `select-table`
+ - 带有选择操作的表格
+ - `isMulti`,是否是多选,默认为`false`
+ - `datas`,数据源,使用前端分页时使用
+ - `columns`,表格列的配置,配置方式同`BaseList`
+ - `filterParams`,搜索项的配置
+ - `pageSize`,每页条目数量,默认为 5
+ - `disabledFunc`,判定哪些条目不可选
+ - `selectedLabel`,表格底部的标签,默认为`已选`
+ - `header`,表格上方的内容
+ - `backendPageStore`,使用后端分页时,数据对应的`store`
+ - `backendPageFunc`,使用后端分页时,获取数据的方法,默认为`fetchListByPage`
+ - `backendPageDataKey`,使用后端分页时,数据在`store`中的位置,默认为`list`
+ - `extraParams`,使用后端分页时,发起请求时的额外参数
+ - `isSortByBack`,是否使用后端排序,默认为`false`
+ - `defaultSortKey`,使用后端排序时,默认的排序键
+ - `defaultSortOrder`,使用后端排序时,默认的排序方向
+ - `initValue`,初始值
+ - `rowKey`,数据的唯一标识,默认为`id`
+ - `onRow`,点击条目时的操作,默认点击条目就会选中该条目
+ - `tabs`,tab 型的表格
+ - `defaultTabValue`,tab 型表格时,默认的 tab
+ - `onTabChange`,tab 型表格时,tab 切换时,调用的函数
+ - 以创建云主机选择安全组`src/pages/compute/containers/Instance/actions/StepCreate/NetworkStep/index.jsx`为例
+ - 这个表格的右侧标题有 tip 提示
+ - 使用后端分页的方式展示数据,并具有额外的参数
+ - 是多选
+ - 这个表格的上方有额外的展示内容
+ - 需要复写`onRow`属性,以免点击表格中的`查看规则`按钮时产生操作冲突
+
+ ```javascript
+ {
+ name: 'securityGroup',
+ label: t('Security Group'),
+ type: 'select-table',
+ tip: t(
+ 'Each instance belongs to at least one security group, which needs to be specified when it is created. Instances in the same security group can communicate with each other on the network, and instances in different security groups are disconnected from the internal network by default.'
+ ),
+ backendPageStore: this.securityGroupStore,
+ extraParams: { project_id: this.currentProjectId },
+ required: true,
+ isMulti: true,
+ header: (
+
+ {t(
+ 'The security group is similar to the firewall function and is used to set up network access control. '
+ )}
+ {t(' You can go to the console to ')}
+
+ {t('create a new security group')}>{' '}
+
+ {t(
+ 'Note: The security group you use will act on all virtual adapters of the instance.'
+ )}
+
+ ),
+ filterParams: securityGroupFilter,
+ columns: securityGroupColumns,
+ onRow: () => {},
+ },
+ ```
+
+ ![select-table](/docs/zh/develop/images/form/select-table.png)
+
+ - 以创建云硬盘选择镜像`src/pages/storage/containers/Volume/actions/Create/index.jsx`为例
+ - 这是带有 Tab 标签的表格,默认展示第一个 tab,tab 切换时会更新数据源
+ - 数据使用前端分页的方式获取,直接配置`datas`即可
+ - 是单选
+ - 配置了已选标签为`已选 镜像`
+
+ ```javascript
+ {
+ name: 'image',
+ label: t('Operating System'),
+ type: 'select-table',
+ datas: this.images,
+ required: sourceTypesIsImage,
+ isMulti: false,
+ hidden: !sourceTypesIsImage,
+ filterParams: [
+ {
+ label: t('Name'),
+ name: 'name',
+ },
+ ],
+ columns: getImageColumns(this),
+ tabs: this.systemTabs,
+ defaultTabValue: this.systemTabs[0].value,
+ selectedLabel: t('Image'),
+ onTabChange: this.onImageTabChange,
+ }
+ ```
+
+ ![select-table-tabs](/docs/zh/develop/images/form/select-table-tabs.png)
+
+ - `input-number`
+ - 数字输入框
+ - `min`,最小值
+ - `max`,最大值
+ - 以创建网络设置 MTU`src/pages/network/containers/Network/actions/CreateNetwork.jsx`为例
+ - 设置了最小、最大值
+
+ ```javascript
+ {
+ name: 'mtu',
+ label: t('MTU'),
+ type: 'input-number',
+ min: 68,
+ max: 9000,
+ extra: t('Minimum value is 68 for IPv4, and 1280 for IPv6.'),
+ },
+ ```
+
+ ![input-number](/docs/zh/develop/images/form/input-number.png)
+
+ - `input-int`
+ - 整数输入框
+ - `min`,最小值
+ - `max`,最大值
+ - 以创建镜像设置最小系统盘`src/pages/compute/containers/Image/actions/Create.jsx`为例
+ - 设置了最小、最大值
+
+ ```javascript
+ {
+ name: 'min_disk',
+ label: t('Min System Disk(GB)'),
+ type: 'input-int',
+ min: 0,
+ max: 500,
+ }
+ ```
+
+ ![input-int](/docs/zh/develop/images/form/input-int.png)
+
+ - `instance-volume`
+ - 云主机硬盘配置组件
+ - `options`,云硬盘类型的选项
+ - `minSize`,云硬盘大小输入框的最小值
+ - 以创建云主机配置系统盘`src/pages/compute/containers/Instance/actions/StepCreate/BaseStep/index.jsx`为例
+
+ ```javascript
+ {
+ name: 'systemDisk',
+ label: t('System Disk'),
+ type: 'instance-volume',
+ options: this.volumeTypes,
+ required: !this.sourceTypeIsVolume,
+ hidden: this.sourceTypeIsVolume,
+ validator: this.checkSystemDisk,
+ minSize: this.getSystemDiskMinSize(),
+ extra: t('Disk size is limited by the min disk of flavor, image, etc.'),
+ onChange: this.onSystemDiskChange,
+ }
+ ```
+
+ ![instance-volume](/docs/zh/develop/images/form/instance-volume.png)
+
+ - `input-password`
+ - 密码输入框
+ - 以创建云主机输入密码`src/pages/compute/containers/Instance/actions/StepCreate/SystemStep/index.jsx`为例
+ - 输入密码,确认密码,并要验证密码格式,以及两次输入数据的一致性
+
+ ```javascript
+ {
+ name: 'password',
+ label: t('Login Password'),
+ type: 'input-password',
+ required: isPassword,
+ hidden: !isPassword,
+ otherRule: getPasswordOtherRule('password', 'instance'),
+ },
+ {
+ name: 'confirmPassword',
+ label: t('Confirm Password'),
+ type: 'input-password',
+ required: isPassword,
+ hidden: !isPassword,
+ otherRule: getPasswordOtherRule('confirmPassword', 'instance'),
+ },
+ ```
+
+ ![input-password](/docs/zh/develop/images/form/input-password.png)
+
+ - `input-name`
+ - 带有格式验证的名称输入框
+ - `placeholder`,输入框的提示语
+ - `isFile`,以文件格式验证名称
+ - `isKeypair`,以密钥支持的格式验证名称
+ - `isStack`,以堆栈支持的格式验证名称
+ - `isImage`,以镜像支持的格式验证名称
+ - `isInstance`,以云主机支持的格式验证名称
+ - 以创建云主机输入名称`src/pages/compute/containers/Instance/actions/StepCreate/SystemStep/index.jsx`为例
+ - 输入密码,确认密码,并要验证密码格式,以及两次输入数据的一致性
+
+ ```javascript
+ {
+ name: 'name',
+ label: t('Name'),
+ type: 'input-name',
+ placeholder: t('Please input name'),
+ required: true,
+ isInstance: true,
+ }
+ ```
+
+ ![input-name](/docs/zh/develop/images/form/input-name.png)
+
+ - `port-range`
+ - 带有验证的 port 输入框
+ - 以安全组创建规则设置源端口/端口范围`src/pages/network/containers/SecurityGroup/Detail/Rule/actions/Create.jsx`为例
+
+ ```javascript
+ {
+ name: 'sourcePort',
+ label: t('Source Port/Port Range'),
+ type: 'port-range',
+ required: showSourcePort,
+ hidden: !showSourcePort,
+ }
+ ```
+
+ ![port-range](/docs/zh/develop/images/form/port-range.png)
+
+ - `more`
+ - 隐藏/展示更多配置项按钮
+ - 以创建云主机系统配置`src/pages/compute/containers/Instance/actions/StepCreate/SystemStep/index.jsx`为例
+
+ ```javascript
+ {
+ name: 'more',
+ label: t('Advanced Options'),
+ type: 'more',
+ }
+ ```
+
+ ![more](/docs/zh/develop/images/form/more.png)
+
+ - `textarea`
+ - 多行文本输入框
+ - 以编辑云硬盘设置描述`src/pages/storage/containers/Volume/actions/Edit.jsx`为例
+
+ ```javascript
+ {
+ name: 'description',
+ label: t('Description'),
+ type: 'textarea',
+ }
+ ```
+
+ ![textarea](/docs/zh/develop/images/form/textarea.png)
+
+ - `upload`
+ - 上传文件输入框
+ - 以创建镜像上传镜像文件`src/pages/compute/containers/Image/actions/Create.jsx`为例
+
+ ```javascript
+ {
+ name: 'file',
+ label: t('File'),
+ type: 'upload',
+ required: true,
+ }
+ ```
+
+ ![upload](/docs/zh/develop/images/form/upload.png)
+
+ - `add-select`
+ - 可以添加、删除条目的表单项
+ - `minCount`,最小数量
+ - `maxCount`,最多数量
+ - `itemComponent`,增加的每个条目使用的组件
+ - `defaultItemValue`,新增条目的默认值
+ - `addText`,添加条目按钮右侧的文字
+ - `addTextTips`,如果`maxCount`存在,随着条目数量的变更,显示的文字
+ - 以创建云主机设置数据盘`src/pages/compute/containers/Instance/actions/StepCreate/BaseStep/index.jsx`为例
+ - 可以设置任意个数的数据盘
+
+ ```javascript
+ {
+ name: 'dataDisk',
+ label: t('Data Disk'),
+ type: 'add-select',
+ options: this.volumeTypes,
+ defaultItemValue: this.defaultVolumeType,
+ itemComponent: InstanceVolume,
+ minCount: 0,
+ addTextTips: t('Data Disks'),
+ addText: t('Add Data Disks'),
+ extra: t(
+ 'Too many disks mounted on the instance will affect the read and write performance. It is recommended not to exceed 16 disks.'
+ ),
+ onChange: this.onDataDiskChange,
+ },
+ ```
+
+ ![add-select](/docs/zh/develop/images/form/add-select.png)
+
+ - `ip-input`
+ - 带有验证功能的 IP 输入框
+ - `version`,ip 类型,默认是`4`,还可设置为`6`
+ - 以云主机挂载网卡手动指定 IP`src/pages/compute/containers/Instance/actions/AttachInterface.jsx`为例
+
+ ```javascript
+ {
+ name: 'ip',
+ label: t('Given IP'),
+ type: 'ip-input',
+ required: ipType === 1,
+ hidden: ipType !== 1,
+ version,
+ // defaultIp,
+ validator: this.checkIP,
+ extra: t('Please make sure this IP address be available.'),
+ }
+ ```
+
+ ![ip-input](/docs/zh/develop/images/form/ip-input.png)
+
+ - `member-allocator`
+ - 负载均衡器中使用的成员选择表单
+ - 以负载均衡器配置成员`src/pages/network/containers/LoadBalancers/StepCreateComponents/MemberStep/index.jsx`为例
+
+ ```javascript
+ {
+ name: 'extMembers',
+ type: 'member-allocator',
+ isLoading: this.store.list.isLoading,
+ ports: this.state.ports,
+ }
+ ```
+
+ ![member-allocator](/docs/zh/develop/images/form/member-allocator.png)
+
+ - `descriptions`
+ - 展示多种信息的表单项
+ - `title`,右侧内容的标题
+ - `onClick`,右侧内容标题旁的跳转按钮
+ - `items`,每个信息展示项的配置,数组
+ - `label`,信息展示项左侧的标题文字
+ - `value`,信息展示项右侧的值
+ - `span`,信息展示右侧占用的布局尺寸
+ - 以创建云主机确认`src/pages/compute/containers/Instance/actions/StepCreate/ConfirmStep/index.jsx`为例
+
+ ```javascript
+ {
+ name: 'confirm-config',
+ label: t('Config Overview'),
+ type: 'descriptions',
+ title: t('Base Config'),
+ onClick: () => {
+ this.goStep(0);
+ },
+ items: [
+ {
+ label: t('Start Source'),
+ value: context.source.label,
+ },
+ {
+ label: t('System Disk'),
+ value: this.getSystemDisk(),
+ },
+ {
+ label: t('Available Zone'),
+ value: context.availableZone.label,
+ },
+ {
+ label: t('Start Source Name'),
+ value: this.getSourceValue(),
+ },
+ {
+ label: t('Data Disk'),
+ value: this.getDataDisk(),
+ },
+ {
+ label: t('Project'),
+ value: context.project,
+ },
+ {
+ label: t('Flavor'),
+ value: this.getFlavor(),
+ },
+ ],
+ }
+ ```
+
+ ![descriptions](/docs/zh/develop/images/form/descriptions.png)
+
+ - `slider-input`
+ - 滑动与输入联动的表单项
+ - `min`,最小值
+ - `max`,最大值
+ - `description`,滑动条下的描述语
+ - 以创建云硬盘设置容量`src/pages/storage/containers/Volume/actions/Create/index.jsx`为例
+
+ ```javascript
+ {
+ name: 'size',
+ label: t('Capacity (GB)'),
+ type: 'slider-input',
+ max: this.maxSize,
+ min: minSize,
+ description: `${minSize}GB-${this.maxSize}GB`,
+ required: this.quotaIsLimit,
+ hidden: !this.quotaIsLimit,
+ onChange: this.onChangeSize,
+ },
+ ```
+
+ ![slider-input](/docs/zh/develop/images/form/slider-input.png)
+
+ - `title`
+ - 展示标题
+ - 以创建堆栈配置参数`src/pages/heat/containers/Stack/actions/Create/Parameter.jsx`为例
+
+ ```javascript
+ {
+ label: t('Fill In The Parameters'),
+ type: 'title',
+ }
+ ```
+
+ ![title](/docs/zh/develop/images/form/title.png)
+
+ - `switch`
+ - 开关
+ - 以创建虚拟网卡设置安全组`src/pages/network/containers/VirtualAdapter/actions/Create.jsx`为例
+
+ ```javascript
+ {
+ name: 'port_security_enabled',
+ label: t('Port Security'),
+ type: 'switch',
+ tip: t(
+ 'Disabling port security will turn off the security group policy protection and anti-spoofing protection on the port. General applicable scenarios: NFV or operation and maintenance Debug.'
+ ),
+ onChange: (e) => {
+ this.setState({
+ port_security_enabled: e,
+ });
+ },
+ }
+ ```
+
+ ![switch](/docs/zh/develop/images/form/switch.png)
+
+ - `check`
+ - checkbox
+ - `content`,输入框右侧的文字
+ - 以云主机修改配置是否强制关机`src/pages/compute/containers/Instance/actions/Resize.jsx`为例
+
+ ```javascript
+ {
+ name: 'option',
+ label: t('Forced Shutdown'),
+ type: 'check',
+ content: t('Agree to force shutdown'),
+ required: true,
+ },
+ ```
+
+ ![check](/docs/zh/develop/images/form/check.png)
+
+ - `transfer`
+ - 穿梭框
+ - `leftTableColumns`,左侧表格的列配置
+ - `rightTableColumns`,右侧表格的列配置
+ - `dataSource`,可供选择的数据源
+ - `showSearch`,是否显示搜索输入框
+ - `oriTargetKeys`,初始化的选中值
+ - `disabled`,是否禁用左侧数据的选中,默认为`false`
+ - 以用户编辑系统角色`src/pages/identity/containers/User/actions/SystemRole.jsx`为例
+ - 左侧是项目名称列表
+ - 右侧是项目名称、对项目配置的角色信息列表
+
+ ```javascript
+ {
+ name: 'select_project',
+ type: 'transfer',
+ label: t('Project'),
+ leftTableColumns: this.leftUserTable,
+ rightTableColumns: this.rightUserTable,
+ dataSource: this.projectList
+ ? this.projectList.filter((it) => it.domain_id === domainDefault)
+ : [],
+ disabled: false,
+ showSearch: true,
+ oriTargetKeys: projectRoles ? Object.keys(projectRoles) : [],
+ }
+ ```
+
+ ![transfer](/docs/zh/develop/images/form/transfer.png)
+
+ - `check-group`
+ - checkbox 组
+ - `options`,配置每个`checkbox`的信息
+ - `label`,每个`checkbox`对应的文字
+ - `value`,`checkbox`对应的键
+ - 以编辑元数据`src/pages/configuration/containers/Metadata/actions/Edit.jsx`为例
+ - 配置 公有、受保护的 属性
+
+ ```javascript
+ {
+ name: 'options',
+ label: t('Options'),
+ type: 'check-group',
+ options: [
+ { label: t('Public'), value: 'isPublic' },
+ { label: t('Protected'), value: 'isProtected' },
+ ],
+ }
+ ```
+
+ ![check-group](/docs/zh/develop/images/form/check-group.png)
+
+ - `textarea-from-file`
+ - 带有读取文件功能的多行文本输入框
+ - 选择文件后,会将文件的内容读取到文本输入框中
+ - 以创建密钥输入公钥信息`src/pages/compute/containers/Keypair/actions/Create.jsx`为例
+ - 配置 公有、受保护的 属性
+
+ ```javascript
+ {
+ name: 'public_key',
+ label: t('Public Key'),
+ type: 'textarea-from-file',
+ hidden: isCreate,
+ required: !isCreate,
+ }
+ ```
+
+ ![textarea-from-file](/docs/zh/develop/images/form/textarea-from-file.png)
+
+ - `ip-distributer`
+ - IP 输入框
+ - `subnets`,选择子网后配置 IP 信息
+ - 可以自动分配 IP,也可手动指定 IP
+ - 可以添加多个 IP
+ - 以创建虚拟网卡设置 IP`src/pages/network/containers/VirtualAdapter/actions/Create.jsx`为例
+
+ ```javascript
+ {
+ name: 'fixed_ips',
+ label: t('Owned Subnet'),
+ type: 'ip-distributer',
+ subnets: subnetDetails,
+ hidden: !network_id,
+ required: true,
+ }
+ ```
+
+ ![ip-distributer](/docs/zh/develop/images/form/ip-distributer.png)
+
+ - `mac-address`
+ - mac 地址输入框
+ - 支持自动分配,也可手动指定
+ - 以编辑虚拟网卡设置 MAC 地址`src/pages/network/containers/VirtualAdapter/actions/Edit.jsx`为例
+
+ ```javascript
+ {
+ name: 'mac_address',
+ label: t('Mac Address'),
+ wrapperCol: { span: 16 },
+ type: 'mac-address',
+ required: true,
+ }
+ ```
+
+ ![mac-address](/docs/zh/develop/images/form/mac-address.png)
+
+ - `network-select-table`
+ - 选择网络的表单项
+ - 分 Tab 展示当前项目网络、共享网络,如果用户具有管理员角色,还可展示全部网络
+ - 以创建虚拟网卡设置网络`src/pages/network/containers/VirtualAdapter/actions/Create.jsx`为例
+
+ ```javascript
+ {
+ name: 'network_id',
+ label: t('Owned Network'),
+ type: 'network-select-table',
+ onChange: this.handleOwnedNetworkChange,
+ required: true,
+ },
+ ```
+
+ ![network-select-table](/docs/zh/develop/images/form/network-select-table.png)
+
+ - `volume-select-table`
+ - 选择硬盘的表单项
+ - 分 Tab 展示可用的、共享的云硬盘
+ - `disabledFunc`,配置什么样的云硬盘不可选
+ - 以云主机挂载硬盘选择硬盘`src/pages/compute/containers/Instance/actions/AttachVolume.jsx`为例
+
+ ```javascript
+ {
+ name: 'volume',
+ label: t('Volume'),
+ type: 'volume-select-table',
+ tip: multiTip,
+ isMulti: false,
+ required: true,
+ serverId: this.item.id,
+ disabledFunc: (record) => {
+ const diskFormat = _get(
+ record,
+ 'origin_data.volume_image_metadata.disk_format'
+ );
+ return diskFormat === 'iso';
+ },
+ }
+ ```
+
+ ![volume-select-table](/docs/zh/develop/images/form/volume-select-table.png)
+
+ - `tab-select-table`
+ - 带有 Tab 的表格型选择表单项
+ - `isMulti`,配置是否为多选
+ - 以申请浮动 IP 选择 Qos`src/pages/network/containers/FloatingIp/actions/Allocate.jsx`为例
+ - 分当前项目、共享的 QoS,如果用户具有管理员权限,也具有所有 QoS 标签项
+
+ ```javascript
+ {
+ name: 'qos_policy_id',
+ label: t('QoS Policy'),
+ type: 'tab-select-table',
+ tabs: getQoSPolicyTabs.call(this),
+ isMulti: false,
+ tip: t('Choosing a QoS policy can limit bandwidth and DSCP'),
+ onChange: this.onQosChange,
+ }
+ ```
+
+ ![tab-select-table](/docs/zh/develop/images/form/tab-select-table.png)
+
+ - `metadata-transfer`
+ - 编辑元数据的表单项
+ - 以镜像编辑元数据`src/pages/compute/containers/Image/actions/ManageMetadata.jsx`为例
+
+ ```javascript
+ {
+ name: 'systems',
+ label: t('Metadata'),
+ type: 'metadata-transfer',
+ metadatas: this.metadatas,
+ validator: (rule, value) => {
+ if (this.hasNoValue(value)) {
+ return Promise.reject(t('Please input value'));
+ }
+ return Promise.resolve();
+ },
+ }
+ ```
+
+ ![metadata-transfer](/docs/zh/develop/images/form/metadata-transfer.png)
+
+ - `aceEditor`
+ - aceEditor
+ - 以创建虚拟网卡编辑 Profile`src/pages/network/containers/VirtualAdapter/actions/Create.jsx`为例
+
+ ```javascript
+ {
+ name: 'bindingProfile',
+ label: t('Binding Profile'),
+ type: 'aceEditor',
+ hidden: !more,
+ mode: 'json',
+ wrapEnabled: true,
+ tabSize: 2,
+ width: '100%',
+ height: '200px',
+ setOptions: {
+ enableBasicAutocompletion: true,
+ enableLiveAutocompletion: true,
+ enableSnippets: true,
+ },
+ validator: (item, value) => {
+ if (value !== undefined && value !== '') {
+ try {
+ JSON.parse(value);
+ return Promise.resolve(true);
+ } catch (e) {
+ return Promise.reject(new Error(t('Illegal JSON scheme')));
+ }
+ }
+ return Promise.resolve(true);
+ },
+ }
+ ```
+
+ ![aceEditor](/docs/zh/develop/images/form/ace-editor.png)
+
+ - `input-json`
+ - 带有 json 格式验证的输入框
+ - 以创建堆栈编辑参数为例`src/resources/stack.js`为例
+
+ ```javascript
+ export const getFormItemType = (type) => {
+ switch (type) {
+ case 'number':
+ return {
+ type: 'input-number',
+ };
+ case 'json':
+ return {
+ type: 'input-json',
+ };
+ case 'boolean':
+ return {
+ type: 'radio',
+ options: yesNoOptions,
+ };
+ default:
+ return {
+ type: 'input',
+ };
+ }
+ };
+ ```
+
+ ![input-json](/docs/zh/develop/images/form/input-json.png)
diff --git a/docs/zh/develop/3-11-Action-introduction.md b/docs/zh/develop/3-11-Action-introduction.md
new file mode 100644
index 00000000..ca8384bc
--- /dev/null
+++ b/docs/zh/develop/3-11-Action-introduction.md
@@ -0,0 +1,141 @@
+简体中文 | [English](/docs/en/develop/3-11-Action-introduction.md)
+
+# 用途
+
+- 配置资源对应的所有操作
+
+ ![操作](/docs/zh/develop/images/form/action.png)
+
+- 按照相应的配置编写后,会在资源列表页相应的位置展示相应的操作按钮
+
+# 代码位置
+
+- `pages/xxxx/containers/XXXX/actions/index.jsx`
+
+# 如何使用
+
+- 返回一个对象,其内配置主操作按钮、批量操作按钮、行操作按钮
+- 以网络`src/pages/network/containers/Network/actions/index.jsx`为例
+ - 配置了主按钮为创建
+ - 配置了批量操作为删除
+ - 配置了行操作为编辑、创建子网、删除
+
+ ```javascript
+ import CreateNetwork from './CreateNetwork';
+ import CreateSubnet from './CreateSubnet';
+ import DeleteAction from './Delete';
+ import Edit from './Edit';
+
+ const actionConfigs = {
+ rowActions: {
+ firstAction: Edit,
+ moreActions: [
+ {
+ action: CreateSubnet,
+ },
+ {
+ action: DeleteAction,
+ },
+ ],
+ },
+ batchActions: [DeleteAction],
+ primaryActions: [CreateNetwork],
+ };
+
+ export default actionConfigs;
+ ```
+
+- 在资源对应的列表代码中配置`actionConfigs`即可
+ - 以网络`src/pages/network/containers/Network/ProjectNetwork.jsx`为例
+
+ ```javascript
+ import actionConfigs from './actions';
+ get actionConfigs() {
+ return actionConfigs;
+ }
+ ```
+
+## 主操作按钮配置`primaryActions`
+
+- 返回组件的列表
+- 如果没有主按钮,可以设置为`null`或`[]`
+- 如果不可操作(比如权限不够),将自动隐藏
+
+## 批量操作按钮配置`batchActions`
+
+- 返回组件的列表
+- 如果没有主按钮,可以设置为`null`或`[]`
+- 如果不可操作(比如权限不够),将自动隐藏
+
+## 行操作按钮配置`rowActions`
+
+- 返回一个对象,内含`firstAction`, `moreActions`对应的组件
+- 批量操作按钮如果被禁用(比如)
+- 可以返回一个空对象`{}`
+- `firstAction`,行操作对应的第一个按钮
+ - 如果不可操作,按钮灰掉
+ - 可以是一个组件
+ - 可以是`null`
+ - 以系统信息-网络`src/pages/configuration/containers/SystemInfo/NeutronAgent/actions/index.jsx`为例
+
+ ```javascript
+ import Enable from './Enable';
+ import Disable from './Disable';
+
+ const actionConfigs = {
+ rowActions: {
+ firstAction: null,
+ moreActions: [
+ {
+ action: Enable,
+ },
+ {
+ action: Disable,
+ },
+ ],
+ },
+ batchActions: [],
+ primaryActions: [],
+ };
+
+ export default actionConfigs;
+ ```
+
+- `moreActions`,`更多`按钮下对应的操作组件
+ - 操作的数组
+ - 其内的操作如果不可用,将直接隐藏该操作按钮
+ - 支持两种格式的配置,对应了不同的展示方案
+ - 每个元素是个含有`action`属性的对象
+
+ ![云硬盘操作](/docs/zh/develop/images/form/volume-action.png)
+
+ - 每个元数是个含有`title`、`actions`属性的对象
+
+ ![云主机操作](/docs/zh/develop/images/form/instance-action.png)
+
+ - 以云主机`src/pages/compute/containers/Instance/actions/index.jsx`为例
+
+ ```javascript
+ const statusActions = [
+ StartAction,
+ StopAction,
+ LockAction,
+ UnlockAction,
+ RebootAction,
+ SoftRebootAction,
+ SuspendAction,
+ ResumeAction,
+ PauseAction,
+ UnpauseAction,
+ Shelve,
+ Unshelve,
+ ];
+ const actionConfigs = {
+ rowActions: {
+ firstAction: Console,
+ moreActions: [
+ {
+ title: t('Instance Status'),
+ actions: statusActions,
+ },...}}
+ ```
diff --git a/docs/zh/develop/3-12-Menu-introduction.md b/docs/zh/develop/3-12-Menu-introduction.md
new file mode 100644
index 00000000..c4713405
--- /dev/null
+++ b/docs/zh/develop/3-12-Menu-introduction.md
@@ -0,0 +1,110 @@
+简体中文 | [English](/docs/en/develop/3-12-Menu-introduction.md)
+
+# 用途
+
+- 点击后直接跳转到相应页面
+- 配置控制台的左侧菜单项
+
+ ![控制台](/docs/zh/develop/images/menu/console-menu.png)
+
+- 配置管理平台的左右菜单项
+
+ ![管理平台](/docs/zh/develop/images/menu/admin-menu.png)
+
+- 支持一级菜单带图标
+- 支持二级菜单展开
+- 支持路由变更后菜单选中项自动切换
+- 支持右侧内容中面包屑的自动处理
+
+# 代码位置
+
+- 控制台的菜单配置`src/layouts/menu.jsx`
+- 管理平台的菜单配置`src/layouts/admin-menu.jsx`
+
+# 如何使用
+
+- 控制台与管理平台的菜单配置,采用相同的配置结构
+- 返回一个 renderMenu 函数,函数返回一个配置数组
+
+## 一级菜单的配置
+
+- `path`
+ - 一级菜单对应的路由
+- `name`
+ - 一级菜单对应的名称
+ - 菜单项中显示的名称
+ - 面包屑中一级菜单对应的名称
+- `key`
+ - 一级菜单对应的 ID 值
+ - 要求具有唯一性
+- `icon`
+ - 一级菜单对应的图标
+ - 菜单完全展开时,显示图标与名称
+ - 菜单折叠时,只显示图标
+- `hasBreadcrumb`
+ - 页面是否展示面包屑
+ - 默认展示面包屑
+ - 以首页为例,`hasBreadcrumb: false`
+- `hasChildren`
+ - 一级菜单是否含有子菜单
+ - 默认值为`true`
+ - 一级菜单可以不包含二级菜单,以`首页`为例
+
+ ```javascript
+ {
+ path: '/base/overview',
+ name: t('Home'),
+ key: '/home',
+ icon: ,
+ hasBreadcrumb: false,
+ hasChildren: false,
+ }
+ ```
+
+ - 一级菜单默认包含二级菜单,以`计算`为例
+
+ ```javascript
+ {
+ path: '/compute',
+ name: t('Compute'),
+ key: '/compute',
+ icon: ,
+ children: [...]
+ }
+ ```
+
+## 二级菜单的配置
+
+- 二级菜单配置在一级菜单的`children`中
+- 详情页、创建页面等不需要展示在菜单项中的页面,配置在二级菜单的`children`中
+- 以云主机类型为例
+
+ ```javascript
+ {
+ path: '/compute/flavor',
+ name: t('Flavor'),
+ key: '/compute/flavor',
+ level: 1,
+ children: [
+ {
+ path: /^\/compute\/flavor\/detail\/.[^/]+$/,
+ name: t('Flavor Detail'),
+ key: 'flavor-detail',
+ level: 2,
+ },
+ ],
+ },
+ ```
+
+- `path`
+ - 菜单对应的路由
+- `name`
+ - 菜单对应的名称
+ - 菜单项中显示的名称
+ - 面包屑中菜单对应的名称
+- `key`
+ - 菜单对应的 ID 值
+ - 要求具有唯一性
+- `level`
+ - 二级菜单的`level=1`
+ - 二级菜单的`children`中的菜单配置`level=2`
diff --git a/docs/zh/develop/3-13-Route-introduction.md b/docs/zh/develop/3-13-Route-introduction.md
new file mode 100644
index 00000000..2c8b52fc
--- /dev/null
+++ b/docs/zh/develop/3-13-Route-introduction.md
@@ -0,0 +1,72 @@
+简体中文 | [English](/docs/en/develop/3-13-Route-introduction.md)
+
+# 用途
+
+- 需要独立展示的页面均需要配置路由
+ - 按产品的需求,菜单下的二级页面,均需要以整页方式展示,如`计算-云主机`
+ - 资源列表页
+ - 如,云主机列表页
+ - 注意,详情页下的相关资源列表页不需要配置路由
+ - 资源详情页
+ - 如,云主机详情页
+ - 整页展示的 Form 表单
+ - 如,创建云主机
+ - 某些菜单只有一级页面,如`首页`,也需要配置路由
+
+# 如何使用
+
+## 二级菜单对应的路由
+
+- 按[目录介绍](2-catalog-introduction.md)中的要求,每个菜单一级页面,在`pages`下有一个独立的文件夹,其内的`containers文件夹`放置二级页面代码,`routes文件夹`配置路由
+- 配置写在`pages/xxxx/routes/index.js`中
+- 路由的配置需要遵守固定格式,可参考`src/pages/compute/routes/index.js`
+ - 是个数组
+ - 数组中的每个元素,要求
+ - `path`, 一级菜单对应的名称,如`计算`用`compute`
+ - `component`,布局组件
+ - `auth`相关的页面,如登录,使用的是`src/layouts/User/index.jsx`组件
+ - 登录后展示的页面,如云主机、页面等,使用的是`src/layouts/Base/index.jsx`组件
+ - 该布局自动处理菜单项的展示、右侧内容头部的`header`展示、右侧内容的面包屑等
+ - `routes`,配置的主体内容,是个数组,每个元素要求
+ - 以计算的路由配置`src/pages/compute/routes/index.js`为例
+
+ ```javascript
+ { path: `${PATH}/instance`, component: Instance, exact: true },
+ ```
+
+ - `path`, 每个整页页面对应的路径,如`compute/instance`
+ - `component`,页面对应的组件,即`containers`下的组件
+
+- 对于资源型的页面,一般会配置
+ - 控制台访问的列表页、详情页、复杂的创建页(简单的创建一般使用弹出窗即可)
+ - 管理平台访问的列表页、详情页(`path`中要求包含`-admin`或`_admin`)
+ - 对于详情页,我们推荐使用`id`参数项
+ - 以云主机`src/pages/compute/routes/index.js`为例
+
+ ```javascript
+ { path: `${PATH}/instance`, component: Instance, exact: true },
+ { path: `${PATH}/instance-admin`, component: Instance, exact: true },
+ {
+ path: `${PATH}/instance/detail/:id`,
+ component: InstanceDetail,
+ exact: true,
+ },
+ {
+ path: `${PATH}/instance-admin/detail/:id`,
+ component: InstanceDetail,
+ exact: true,
+ },
+ { path: `${PATH}/instance/create`, component: StepCreate, exact: true },
+ ```
+
+## 一级菜单对应的路由
+
+- 一级菜单需要添加在`src/core/routes.js`
+- 以计算为例
+
+ ```javascript
+ {
+ path: '/compute',
+ component: Compute,
+ },
+ ```
diff --git a/docs/zh/develop/3-14-I18n-introduction.md b/docs/zh/develop/3-14-I18n-introduction.md
new file mode 100644
index 00000000..e07f2d0e
--- /dev/null
+++ b/docs/zh/develop/3-14-I18n-introduction.md
@@ -0,0 +1,41 @@
+简体中文 | [English](/docs/en/develop/3-14-I18n-introduction.md)
+
+# 用途
+
+- 框架支持国际化,默认支持英文、中文
+
+ ![i18n](/docs/zh/develop/images/i18n/i18n.png)
+
+ ![english](/docs/zh/develop/images/i18n/english.png)
+
+# 代码位置
+
+- `src/locales/index.js`
+- 英文:`src/locales/en.json`
+- 中文:`src/locales/zh.json`
+
+# 如何使用
+
+- 代码中的需要国际化展示的字符串均使用英文,使用命令行完成字符采集后,无需更新 en.json 文件,只需要修改 zh.json 中对应的中文即可完成国际化的操作
+- 对于需要国际化的字符串,使用`t`函数即可
+ - 以`云主机`为例,对应的国际化写法为`t('instance')`
+ - 注意,英文是大小写相关的
+ - `t`函数支持带有参数的字符串
+ - 参数使用`{}`标识,如
+
+ ```javascript
+ confirmContext = () =>
+ t('Are you sure to { action }?', {
+ action: this.actionName || this.title,
+ });
+ ```
+
+- 采集
+
+ ```shell
+ yarn run i18n
+ ```
+
+ - 采集后,`en.json`与`zh.json`文件会自动更新
+- 更新中文
+ - 采集后,直接在`zh.json`中更新相应的中文翻译即可
diff --git a/docs/zh/develop/3-2-BaseTabList-introduction.md b/docs/zh/develop/3-2-BaseTabList-introduction.md
new file mode 100644
index 00000000..35ddfe65
--- /dev/null
+++ b/docs/zh/develop/3-2-BaseTabList-introduction.md
@@ -0,0 +1,123 @@
+简体中文 | [English](/docs/en/develop/3-2-BaseTabList-introduction.md)
+
+# 用途
+
+- 各可切换列表页的基类
+
+ ![Tab列表页](/docs/zh/develop/images/list/tab-list.png)
+
+- 支持切换时自动处理数据展示
+
+# BaseTabList 代码文件
+
+- `src/containers/TabList/index.jsx`
+
+# BaseTabList 属性与函数定义介绍
+
+- 带有 Tab 切换的资源列表继承于 BaseTabList
+- 只需要复写部分函数即可完成页面的开发
+- 属性与函数分为以下四种,
+ - 需要复写的属性与函数,主要包含:
+ - 页面内的`Tab`配置
+ - 按需复写的函数与属性,主要包含:
+ - 页面内的权限配置
+ - 无需复写的函数与属性,主要包含:
+ - 当前页是否是管理平台页面
+ - 基类中的基础函数,主要包含:
+ - 渲染页面
+ - 处理切换 Tab 时的路由变动
+ - 更详细与全面的介绍见下
+
+## 需要复写的属性与函数
+
+- `tabs`:
+ - 需要复写该函数
+ - 用于配置页面内的 Tab
+ - 每个 Tab 的配置项:
+ - `title`,Tab 标签上的标题
+ - `key`,每个 Tab 的唯一标识
+ - `component`,每个 Tab 对应的组件,基本都是继承于`BaseList`的资源列表组件
+ - 返回 Tab 配置的列表
+ - 页面默认显示 Tab 列表中的第一个`component`
+ - 以镜像`src/pages/compute/containers/Image/index.jsx`为例
+
+ ```javascript
+ get tabs() {
+ const tabs = [
+ {
+ title: t('Current Project Image'),
+ key: 'project',
+ component: Image,
+ },
+ {
+ title: t('Public Image'),
+ key: 'public',
+ component: Image,
+ },
+ {
+ title: t('Shared Image'),
+ key: 'shared',
+ component: Image,
+ },
+ ];
+ if (this.hasAdminRole) {
+ tabs.push({
+ title: t('All Image'),
+ key: 'all',
+ component: Image,
+ });
+ }
+ return tabs;
+ }
+ ```
+
+## 按需复写的属性与函数
+
+- 以下涉及到的属性与函数,一般均不需要配置
+ - 目前只在 VPN 页面(`src/pages/network/containers/VPN/index.jsx`)使用,该页面使用这些配置判定权限,以及判定失败时展示使用
+- `name`
+ - 整个 Tab 页面的全称
+ - 以 VPN `src/pages/network/containers/VPN/index.jsx`为例
+
+ ```javascript
+ get name() {
+ return t('VPN');
+ }
+ ```
+
+- `checkEndpoint`
+ - 是否需要验证该页面对应服务的 endpoint
+ - 默认值是`false`
+ - 以 VPN `src/pages/network/containers/VPN/index.jsx`为例
+
+ ```javascript
+ get checkEndpoint() {
+ return true;
+ }
+ ```
+
+- `endpoint`
+ - 该页面对应服务的 endpoint
+ - 仅在`checkEndpoint=true`时有用
+ - 以 VPN `src/pages/network/containers/VPN/index.jsx`为例
+
+ ```javascript
+ get endpoint() {
+ return vpnEndpoint();
+ }
+ ```
+
+## 不需要复写的属性与函数
+- `location`
+ - 页面的路由信息
+- `isAdminPage`
+ - 当前页面是否是“管理平台”的页面
+- `hasAdminRole`
+ - 登录的用户角色是否具有管理员角色
+- `getUrl`
+ - 生成页面 Url 的函数
+ - 如:需要给列表页的关联资源提供跳转功能,使用该函数,可以在控制台跳转到控制台的相应地址,在管理平台跳转到管理平台的相应地址
+
+## 基类中的基础函数
+
+- 建议查看代码理解,`src/containers/TabList/index.jsx`
diff --git a/docs/zh/develop/3-3-BaseDetail-introduction.md b/docs/zh/develop/3-3-BaseDetail-introduction.md
new file mode 100644
index 00000000..32a10761
--- /dev/null
+++ b/docs/zh/develop/3-3-BaseDetail-introduction.md
@@ -0,0 +1,259 @@
+简体中文 | [English](/docs/en/develop/3-3-BaseDetail-introduction.md)
+
+# 用途
+
+![详情页](/docs/zh/develop/images/detail/volume.png)
+
+- 各资源详情页的基类
+- 支持返回列表页
+- 支持与列表页一致的数据操作
+- 支持详情页头部的展示与折叠
+- 支持基于 Tab 形式展示的基本信息与相关资源信息
+- 支持上下分层的展示方案
+- 需要复写部分函数即可完成页面的开发
+
+# BaseDetail 代码文件
+
+- `src/containers/TabDetail/index.jsx`
+
+# BaseDetail 属性与函数定义介绍
+
+- 资源详情继承于 BaseDetail 组件
+- 代码位置:`pages/xxxx/containers/XXXX/Detail/index.jsx`
+- 只需要复写部分函数即可完成页面的开发
+- 属性与函数分为以下四种,
+ - 通常需要复写的属性与函数,主要包含:
+ - 详情页的权限
+ - 详情页的资源名称
+ - 详情页对应的列表页
+ - 详情页的操作配置
+ - 详情页的上方信息配置
+ - 详情页的下方 Tab 页面配置
+ - 详情页对应的`store`
+ - 按需复写的函数与属性,主要包含:
+ - 详情页操作对应的数据
+ - 获取详情数据的参数
+ - 获取详情数据的函数
+ - 无需复写的函数与属性,主要包含:
+ - 当前页是否是管理平台页面
+ - 基类中的基础函数,主要包含:
+ - 渲染页面
+ - 下方列表页数据变动时详情数据的自动刷新
+ - 折叠/展开头部信息
+ - 更详细与全面的介绍见下
+
+## 通常需要复写的属性与函数
+
+- `policy`:
+ - 必须复写该函数
+ - 页面对应的权限,如果权限验证不通过,则无法请求数据。
+ - 以云硬盘`src/pages/storage/containers/Volume/Detail/index.jsx`为例
+
+ ```javascript
+ get policy() {
+ return 'volume:get';
+ }
+ ```
+
+- `name`
+ - 必须复写该函数
+ - 页面资源对应的名称。
+ - 以云硬盘`src/pages/storage/containers/Volume/Detail/index.jsx`为例
+
+ ```javascript
+ get name() {
+ return t('volume');
+ }
+ ```
+
+- `listUrl`
+ - 该详情页对应的资源列表页
+ - 以云硬盘`src/pages/storage/containers/Volume/Detail/index.jsx`为例
+
+ ```javascript
+ get listUrl() {
+ return this.getUrl('/storage/volume');
+ }
+ ```
+
+- `actionConfigs`
+ - 配置资源的各种操作
+ - 对数据的操作
+ - 配置定义在资源的 actions 目录下
+ - 一般直接使用与资源列表页相一致的配置即可
+ - 以密钥`src/pages/compute/containers/Keypair/Detail/index.jsx`为例
+
+ ```javascript
+ import actionConfigs from '../actions';
+ get actionConfigs() {
+ return actionConfigs;
+ }
+ ```
+
+- `detailInfos`
+ - 详情页上方的信息
+ - 是一个配置列表
+ - 每个配置
+ - `title`,必须项,标题
+ - `dataIndex`,必须项,对应于数据的 Key
+ - `render`,可选项,默认是基于`dataIndex`来展示内容,使用该属性,可基于`render`的结果渲染表格内容
+ - `valueRender`,可选项,基于`dataIndex`及`valueRender`生成展示数据
+ - `sinceTime`,处理时间,显示成"XX 小时前"
+ - `keepTime`,显示剩余时间
+ - `yesNo`,处理`Boolean`值,显示成“是”、“否”
+ - `GBValue`,处理大小,显示成"XXXGB"
+ - `noValue`,没有值时,显示成“-”
+ - `bytes`,处理大小
+ - `uppercase`,大写
+ - `formatSize`,处理大小,显示如“2.32 GB”,“56.68 MB”
+ - `toLocalTime`,处理时间,显示如“2021-06-17 04:13:07”
+ - `toLocalTimeMoment`,处理时间,显示如“2021-06-17 04:13:07”
+ - 以云硬盘`src/pages/storage/containers/Volume/Detail/index.jsx`为例
+
+ ```javascript
+ get detailInfos() {
+ return [
+ {
+ title: t('Name'),
+ dataIndex: 'name',
+ },
+ {
+ title: t('Description'),
+ dataIndex: 'description',
+ },
+ {
+ title: t('Shared'),
+ dataIndex: 'multiattach',
+ valueRender: 'yesNo',
+ },
+ {
+ title: t('Status'),
+ dataIndex: 'status',
+ render: (value) => volumeStatus[value] || '-',
+ },
+ {
+ title: t('Size'),
+ dataIndex: 'size',
+ },
+ {
+ title: t('Created At'),
+ dataIndex: 'created_at',
+ valueRender: 'toLocalTime',
+ },
+ {
+ title: t('Type'),
+ dataIndex: 'volume_type',
+ },
+ {
+ title: t('Encrypted'),
+ dataIndex: 'encrypted',
+ valueRender: 'yesNo',
+ },
+ ];
+ }
+ ```
+
+- `tabs`
+ - 详情页下方的 Tab 配置
+ - 每个 Tab 的配置项:
+ - `title`,Tab 标签上的标题
+ - `key`,每个 Tab 的唯一标识
+ - `component`,每个 Tab 对应的组件,基本都是继承于`BaseList`的资源列表组件
+ - 返回 Tab 配置的列表
+ - 页面默认显示 Tab 列表中的第一个`component`
+ - 通常,基础信息继承于`BaseDetail`类
+ - 通常,详情页中的资源列表页直接复用资源列表即可,只需同步处理下列表页内的参数请求即可
+ - 以云硬盘详情页中的备份列表`src/pages/storage/containers/Backup/index.jsx`为例
+
+ ```javascript
+ updateFetchParamsByPage = (params) => {
+ if (this.isInDetailPage) {
+ const { id, ...rest } = params;
+ return {
+ volume_id: id,
+ ...rest,
+ };
+ }
+ return params;
+ };
+ ```
+
+ - 以云硬盘`src/pages/storage/containers/Volume/Detail/index.jsx`为例
+
+ ```javascript
+ get tabs() {
+ const tabs = [
+ {
+ title: t('Detail'),
+ key: 'base',
+ component: BaseDetail,
+ },
+ {
+ title: t('Backup'),
+ key: 'backup',
+ component: Backup,
+ },
+ {
+ title: t('Snapshot'),
+ key: 'snapshot',
+ component: Snapshot,
+ },
+ ];
+ return tabs;
+ }
+ ```
+
+- `init`
+ - 配置 Store 的函数,在这个函数中配置用于处理数据请求的 Store
+ - 一般使用的是`new XXXStore()`形式
+ - 以云硬盘`src/pages/storage/containers/Volume/Detail/index.jsx`为例
+
+ ```javascript
+ init() {
+ this.store = new VolumeStore();
+ }
+ ```
+
+## 按需复写的属性与函数
+
+- `fetchData`
+ - 详情页中的获取数据的函数
+ - 不建议重写该方法
+ - 默认使用`this.store.fetchDetail`获取数据
+- `updateFetchParams`
+ - 更新数据请求的参数
+ - 一般配合 store 中的`detailDidFetch`使用
+ - 以云主机`src/pages/compute/containers/Instance/Detail/index.jsx`为例
+
+ ```javascript
+ updateFetchParams = (params) => ({
+ ...params,
+ isRecycleBinDetail: this.isRecycleBinDetail,
+ });
+ ```
+
+## 不需要复写的属性与函数
+
+- `params`
+ - 路由带有的参数信息
+ - 一般用于生成页面请求 API 时的参数
+- `id`
+ - 路由信息中的`id`
+- `isAdminPage`
+ - 当前页面是否是“管理平台”的页面
+- `getUrl`
+ - 生成页面 Url 的函数
+ - 如:需要给关联资源提供跳转功能,使用该函数,可以在控制台跳转到控制台的相应地址,在管理平台跳转到管理平台的相应地址
+- `routing`
+ - 页面对应的路由信息
+- `isLoading`
+ - 当前页面是否在数据更新,更新时会显示 loading 样式
+- `tab`
+ - 当前展示的下方 Tab 页面信息
+- `detailData`
+ - 页面内展示的数据信息
+ - 来源于`this.store.detail`
+
+## 基类中的基础函数
+
+- 建议查看代码理解,`src/containers/TabDetail/index.jsx`
diff --git a/docs/zh/develop/3-4-BaseDetailInfo-introduction.md b/docs/zh/develop/3-4-BaseDetailInfo-introduction.md
new file mode 100644
index 00000000..65ff5414
--- /dev/null
+++ b/docs/zh/develop/3-4-BaseDetailInfo-introduction.md
@@ -0,0 +1,144 @@
+简体中文 | [English](/docs/en/develop/3-4-BaseDetailInfo-introduction.md)
+
+# 用途
+
+![详情信息页](/docs/zh/develop/images/detail/image-detail-info.png)
+
+- 各资源详情页中详情 Tab 中组件的基类
+- 左右结构展示
+- 以 Card 的形式展示
+- 以配置 Card 的方式即可完成页面内容的展示
+
+# BaseDetailInfo 代码文件
+
+- `src/containers/BaseDetail/index.jsx`
+
+# BaseDetailInfo 属性与函数定义介绍
+
+- 资源详情信息继承于 BaseDetailInfo
+- 代码位置:`pages/xxxx/containers/XXXX/Detail/BaseDetail.jsx`
+- 只需要复写部分函数即可完成页面的开发
+- 属性与函数分为以下四种,
+ - 通常需要复写的属性与函数,主要包含:
+ - 左侧的 Card 列表
+ - 按需复写的函数与属性,主要包含:
+ - 右侧的 Card 列表
+ - 获取数据的函数
+ - 展示数据的来源
+ - 无需复写的函数与属性,主要包含:
+ - 当前页是否是管理平台页面
+ - 基类中的基础函数,主要包含:
+ - 渲染页面
+ - 更详细与全面的介绍见下
+
+## Card 的配置
+
+- 页面中左侧、右侧的 Card 均采用相同的配置方式
+- 每个 Card 的配置如下,
+ - `title`,必须项,Card 的标题
+ - `titleHelp`, 可选项,Card 的标题旁显示的提示信息
+ - `render`,可选项,如果存在,则基于`render`渲染 Card 的内容
+ - `options`,可选项,Card 中每行的配置列表,每个 option 配置如下,
+ - `label`,必须项,行中的标签
+ - `dataIndex`,必须项,对应于`this.detailData`中的 key,默认是基于`dataIndex`展示行内的数据
+ - `render`,可选项,可基于`render`的结果渲染行内的内容
+ - `valueRender`,可选项,基于`dataIndex`及`valueRender`生成行内的展示数据
+ - `sinceTime`,处理时间,显示成"XX 小时前"
+ - `keepTime`,显示剩余时间
+ - `yesNo`,处理`Boolean`值,显示成“是”、“否”
+ - `GBValue`,处理大小,显示成"XXXGB"
+ - `noValue`,没有值时,显示成“-”
+ - `bytes`,处理大小
+ - `uppercase`,大写
+ - `formatSize`,处理大小,显示如“2.32 GB”,“56.68 MB”
+ - `toLocalTime`,处理时间,显示如“2021-06-17 04:13:07”
+ - `toLocalTimeMoment`,处理时间,显示如“2021-06-17 04:13:07”
+ - `copyable`,可选项,该行内的数据是否可复制,如可复制,会显示复制 icon
+- 以密钥`src/pages/compute/containers/Keypair/Detail/BaseDetail.jsx`为例
+
+ ```javascript
+ get keypairInfoCard() {
+ const options = [
+ {
+ label: t('Fingerprint'),
+ dataIndex: 'fingerprint',
+ },
+ {
+ label: t('Public Key'),
+ dataIndex: 'public_key',
+ copyable: true,
+ },
+ {
+ label: t('User ID'),
+ dataIndex: 'user_id',
+ },
+ ];
+ return {
+ title: t('Keypair Info'),
+ options,
+ };
+ }
+ ```
+
+## 通常需要复写的属性与函数
+
+- `leftCards`:
+ - 必须复写该函数
+ - 左侧展示的 Card 列表
+ - 以镜像`src/pages/compute/containers/Image/Detail/BaseDetail.jsx`为例
+
+ ```javascript
+ get leftCards() {
+ const cards = [this.baseInfoCard, this.securityCard];
+ return this.isImageDetail ? cards : [this.InstanceCard, ...cards];
+ }
+ ```
+
+- `init`
+ - 配置 Store 的函数,在这个函数中配置用于处理数据请求的
+ Store,如果配置了该函数,则会在展示该页面时发起数据请求,但是有时展示该页面时,并不需要额外请求,只需要使用`this.props.detail`即可
+ - 一般使用的是`new XXXStore()`形式
+ - 以镜像`src/pages/compute/containers/Image/Detail/BaseDetail.jsx`为例
+
+ ```javascript
+ init() {
+ this.store = new ImageStore();
+ }
+ ```
+
+## 按需复写的属性与函数
+
+- `rightCards`
+ - 右侧展示的 Card 列表
+- `fetchData`
+ - 获取 Card 数据的函数
+ - 一般不需要复写该函数
+- `detailData`
+ - 页面 Card 的数据来源
+ - 默认是`this.props.detail || toJS(this.store.detail)`
+ - 一般不需要复写该函数
+ - 以云硬盘的 Qos`src/pages/storage/containers/VolumeType/QosSpec/Detail/index.jsx`为例
+
+ ```javascript
+ get detailData() {
+ return this.store.detail.qos_specs;
+ }
+ ```
+
+## 不需要复写的属性与函数
+
+- `id`
+ - 路由信息中的`id`
+- `isAdminPage`
+ - 当前页面是否是“管理平台”的页面
+- `getUrl`
+ - 生成页面 Url 的函数
+ - 如:需要给关联资源提供跳转功能,使用该函数,可以在控制台跳转到控制台的相应地址,在管理平台跳转到管理平台的相应地址
+- `routing`
+ - 页面对应的路由信息
+- `isLoading`
+ - 当前页面是否在数据更新,更新时会显示 loading 样式
+
+## 基类中的基础函数
+
+- 建议查看代码理解,`src/containers/BaseDetail/index.jsx`
diff --git a/docs/zh/develop/3-5-BaseStore-introduction.md b/docs/zh/develop/3-5-BaseStore-introduction.md
new file mode 100644
index 00000000..1b3a882b
--- /dev/null
+++ b/docs/zh/develop/3-5-BaseStore-introduction.md
@@ -0,0 +1,554 @@
+简体中文 | [English](/docs/en/develop/3-5-BaseStore-introduction.md)
+
+# 用途
+
+- 数据请求的处理
+- 支持获取全部数据
+- 支持分页获取数据
+- 支持对数据的各种请求处理(PUT、POST、GET、PATCH、DELETE、HEAD 等)
+
+# BaseStore 代码文件
+
+- `src/stores/base.js`
+
+# BaseStore 属性与函数定义介绍
+
+- 资源数据的 Store 继承于 BaseStore 类
+- 代码位置:`src/stores/xxx/xxx.js`,如云主机对应的 store 在`src/stores/nova/instance.js`
+- 只需要复写部分函数即可完成数据的请求操作
+- 属性与函数分为以下四种,
+ - 通常需要复写的属性与函数,主要包含:
+ - 与生成 url 相关的属性与函数
+ - 按需复写的函数与属性,主要包含:
+ - 列表数据的再加工
+ - 详情数据的再加工
+ - 请求参数的处理
+ - url 的处理
+ - 无需复写的函数与属性,主要包含:
+ - 清空数据
+ - 封装数据时对项目信息的处理
+ - 基类中的基础函数,主要包含:
+ - 处理分页数据的`marker`
+ - 更详细与全面的介绍见下
+
+## 名词说明
+
+- 前端分页
+ - 一次性从后端获取所有列表数据
+ - 前端基于获取到的数据总量、页面内配置的当前页数、每页数量来展示数据(`BaseList`组件处理)
+- 后端分页
+ - 以当前页号、每页数量向后端请求数据
+- 前端排序
+ - 使用前端分页时,按设定的排序信息对所有数据排序
+ - 使用后端分页时,按设定的排序信息对当前页内的数据排序
+- 后端排序
+ - 以当前页号、每页数量、当前排序信息向后端请求数据
+ - 不存在前端分页+后端排序这种组合方式
+
+## 通常需要复写的属性与函数
+
+- `module`:
+ - 必须复写该函数
+ - 资源对应的模块
+ - 该函数用于生成请求的 url
+ - 以云主机`src/stores/nova/instance.js`为例
+
+ ```javascript
+ get module() {
+ return 'servers';
+ }
+ ```
+
+- `apiVersion`
+ - 必须复写该函数
+ - 资源对应的 api 前缀
+ - 因所有的请求需要由服务端转发,所以,api 的前缀需要基于`profile`内的信息生成
+ - 以云主机`src/stores/nova/instance.js`为例
+
+ ```javascript
+ get apiVersion() {
+ return novaBase();
+ }
+ ```
+
+- `responseKey`
+ - 必须复写该函数
+ - 用于生成数据返回的 key,创建的 key 等
+ - 以云主机`src/stores/nova/instance.js`为例
+
+ ```javascript
+ get responseKey() {
+ return 'server';
+ }
+ ```
+
+ ![请求](/docs/zh/develop/images/store/response-key.png)
+
+## 按需复写的属性与函数
+
+- `listDidFetch`
+ - 列表数据二次加工使用的函数
+ - 可请求其他 API 后,整合数据
+ - 可过滤数据
+ - 请求某个指定云硬盘的快照列表时,可以基于`filters`中的参数再次过滤数据
+ - 以云硬盘快照`src/stores/cinder/snapshot.js`为例
+
+ ```javascript
+ async listDidFetch(items, allProjects, filters) {
+ if (items.length === 0) {
+ return items;
+ }
+ const { id } = filters;
+ const datas = id ? items.filter((it) => it.volume_id === id) : items;
+ return datas;
+ }
+ ```
+
+ - 如果需要显示加密信息,需要发起额外请求后,整合数据
+ - 以云硬盘类型`src/stores/cinder/volume-type.js`为例
+
+ ```javascript
+ async listDidFetch(items, allProjects, filters) {
+ const { showEncryption } = filters;
+ if (items.length === 0) {
+ return items;
+ }
+ if (!showEncryption) {
+ return items;
+ }
+ const promiseList = items.map((i) =>
+ request.get(`${this.getDetailUrl({ id: i.id })}/encryption`)
+ );
+ const encryptionList = await Promise.all(promiseList);
+ const result = items.map((i) => {
+ const { id } = i;
+ const encryption = encryptionList.find((e) => e.volume_type_id === id);
+ return {
+ ...i,
+ encryption,
+ };
+ });
+ return result;
+ }
+ ```
+
+- `detailDidFetch`
+ - 详情数据二次加工使用的函数
+ - 可请求其他 API 后,整合数据
+ - 以云硬盘快照`src/stores/cinder/snapshot.js`为例
+
+ ```javascript
+ async detailDidFetch(item) {
+ const { volume_id } = item;
+ const volumeUrl = `${cinderBase()}/${
+ globals.user.project.id
+ }/volumes/${volume_id}`;
+ const { volume } = await request.get(volumeUrl);
+ item.volume = volume;
+ return item;
+ }
+ ```
+
+- `listResponseKey`
+ - 列表数据的返回 Key
+ - 默认是`${this.responseKey}s`
+ - 以云硬盘快照`src/stores/cinder/snapshot.js`为例
+
+ ```javascript
+ get responseKey() {
+ return 'snapshot';
+ }
+
+ get listResponseKey() {
+ return 'volume_snapshots';
+ }
+ ```
+
+- `getListUrl`
+ - 请求数据使用的 url
+ - 前端分页请求列表数据(即一次性获取所有数据)时,优先使用`this.getListDetailUrl()`
+ - 后端分页请求列表数据时,按优先级`this.getListPageUrl()` > `this.getListDetailUrl()` > `this.getListUrl()`
+ - 默认值为
+
+ ```javascript
+ getListUrl = () => `${this.apiVersion}/${this.module}`;
+ ```
+
+ - 以 Heat 的堆栈的日志`src/stores/heat/event.js`为例
+
+ ```javascript
+ getListUrl = ({ id, name }) =>
+ `${this.apiVersion}/${this.module}/${name}/${id}/events`;
+ ```
+
+- `getListDetailUrl`
+ - 请求数据使用的 url
+ - 前端分页请求列表数据(即一次性获取所有数据)时,优先使用`this.getListDetailUrl()`
+ - 后端分页请求列表数据时,按优先级`this.getListPageUrl()` > `this.getListDetailUrl()` > `this.getListUrl()`
+ - 默认值为
+
+ ```javascript
+ getListDetailUrl = () => '';
+ ```
+
+ - 以云硬盘`src/stores/cinder/volume.js`为例
+
+ ```javascript
+ getListDetailUrl = () => `${skylineBase()}/extension/volumes`;
+ ```
+
+- `getListPageUrl`
+ - 后端分页数据使用的 url
+ - 后端分页请求列表数据时,按优先级`this.getListPageUrl()` > `this.getListDetailUrl()` > `this.getListUrl()`
+ - 默认值为
+
+ ```javascript
+ getListPageUrl = () => '';
+ ```
+
+ - 以云硬盘`src/stores/cinder/volume.js`为例
+
+ ```javascript
+ getListPageUrl = () => `${skylineBase()}/extension/volumes`;
+ ```
+
+- `getDetailUrl`
+ - 详情数据对应的 url
+ - 使用 rest 风格的 API,所以,该 url 也是 put, delete, patch 对应的 url
+ - 默认值为
+
+ ```javascript
+ getDetailUrl = ({ id }) => `${this.getListUrl()}/${id}`;
+ ```
+
+ - 以堆栈`src/stores/heat/stack.js`为例
+
+ ```javascript
+ getDetailUrl = ({ id, name }) => `${this.getListUrl()}/${name}/${id}`;
+ ```
+
+- `needGetProject`
+ - 对服务端返回的数据是否需要二次加工其中的项目信息
+ - 一般 Openstack API 返回的数据只有`project_id`信息,按页面展示的需求,在管理平台需要展示项目名称
+ - 默认值是`true`
+ - 以元数据`src/stores/glance/metadata.js`为例
+
+ ```javascript
+ get needGetProject() {
+ return false;
+ }
+ ```
+
+- `mapper`
+ - 对服务端返回的列表、详情数据做二次加工
+ - 一般是为了更便捷的在资源列表、资源详情中展示数据使用
+ - 默认值为
+
+ ```javascript
+ get mapper() {
+ return (data) => data;
+ }
+ ```
+
+ - 以云硬盘`src/stores/cinder/volume.js`为例
+
+ ```javascript
+ get mapper() {
+ return (volume) => ({
+ ...volume,
+ disk_tag: isOsDisk(volume) ? 'os_disk' : 'data_disk',
+ description: volume.description || (volume.origin_data || {}).description,
+ });
+ }
+ ```
+
+- `mapperBeforeFetchProject`
+ - 在处理项目信息前,对服务端返回的列表、详情数据做二次加工
+ - 一般是为了处理返回数据中的项目信息使用
+ - 默认值为
+
+ ```javascript
+ get mapperBeforeFetchProject() {
+ return (data) => data;
+ }
+ ```
+
+ - 以镜像`src/stores/glance/image.js`为例
+
+ ```javascript
+ get mapperBeforeFetchProject() {
+ return (data, filters, isDetail) => {
+ if (isDetail) {
+ return {
+ ...data,
+ project_id: data.owner,
+ };
+ }
+ return {
+ ...data,
+ project_id: data.owner,
+ project_name: data.owner_project_name || data.project_name,
+ };
+ };
+ }
+ ```
+
+- `paramsFunc`
+ - 前端分页请求(即`fetchList`)时,对请求参数的更新
+ - 默认是对从资源列表代码(`pages/xxxx/xxx/index.jsx`)使用`fetchList`时,参数的过滤
+ - 默认值
+
+ ```javascript
+ get paramsFunc() {
+ if (this.filterByApi) {
+ return (params) => params;
+ }
+ return (params) => {
+ const reservedKeys = [
+ 'all_data',
+ 'all_projects',
+ 'device_id',
+ 'network_id',
+ 'floating_network_id',
+ 'start_at_gt',
+ 'start_at_lt',
+ 'binary',
+ 'fixed_ip_address',
+ 'device_owner',
+ 'project_id',
+ 'type',
+ 'sort',
+ 'security_group_id',
+ 'id',
+ 'security_group_id',
+ 'owner_id',
+ 'status',
+ 'fingerprint',
+ 'resource_types',
+ 'floating_ip_address',
+ 'uuid',
+ 'loadbalancer_id',
+ 'ikepolicy_id',
+ 'ipsecpolicy_id',
+ 'endpoint_id',
+ 'peer_ep_group_id',
+ 'local_ep_group_id',
+ 'vpnservice_id',
+ ];
+ const newParams = {};
+ Object.keys(params).forEach((key) => {
+ if (reservedKeys.indexOf(key) >= 0) {
+ newParams[key] = params[key];
+ }
+ });
+ return newParams;
+ };
+ }
+ ```
+
+ - 以云硬盘`src/stores/cinder/volume.js`为例
+
+ ```javascript
+ get paramsFunc() {
+ return (params) => {
+ const { serverId, ...rest } = params;
+ return rest;
+ };
+ }
+ ```
+
+- `paramsFuncPage`
+ - 后端分页请求(即`fetchListByPage`)时,对请求参数的更新
+ - 默认是对从资源列表代码(`pages/xxxx/xxx/index.jsx`)使用`fetchListByPage`时,参数的过滤
+ - 默认值
+
+ ```javascript
+ get paramsFuncPage() {
+ return (params) => {
+ const { current, ...rest } = params;
+ return rest;
+ };
+ }
+ ```
+
+ - 以云硬盘类型`src/stores/cinder/volume-type.js`为例
+
+ ```javascript
+ get paramsFuncPage() {
+ return (params) => {
+ const { current, showEncryption, ...rest } = params;
+ return rest;
+ };
+ }
+ ```
+
+- `fetchListByLimit`
+ - 前端分页请求所有数据时,是否要基于`limit`发起多个请求,最终实现所有数据的获取
+ - Openstack API 默认返回的是 1000 个数据,对于某些资源数据很多的情况,需要使用该配置以便获取到所有数据
+ - 默认值是`false`
+ - 以镜像`src/stores/glance/image.js`为例
+
+ ```javascript
+ get fetchListByLimit() {
+ return true;
+ }
+ ```
+
+- `markerKey`
+ - 后端分页请求数据时,marker 的来源
+ - 因为对 Openstack 的请求是由后端转发的,所以并没有直接使用列表数据返回的 Openstack 拼接好的下一页数据应该使用的 Url,而是根据返回的数据,解析出`marker`
+ - 默认值是`id`
+ - 通常不需要复写
+ - 以密钥`src/stores/nova/keypair.js`为例
+
+ ```javascript
+ get markerKey() {
+ return 'keypair.name';
+ }
+ ```
+
+- `requestListByMarker`
+ - 后端分页时,使用`marker`请求分页下的数据
+ - 通常不需要复写
+ - 默认值是
+
+ ```javascript
+ async requestListByMarker(url, params, limit, marker) {
+ const newParams = {
+ ...params,
+ limit,
+ };
+ if (marker) {
+ newParams.marker = marker;
+ }
+ return request.get(url, newParams);
+ }
+ ```
+
+ - 以云主机组`src/stores/nova/server-group.js`为例
+
+ ```javascript
+ async requestListByMarker(url, params, limit, marker) {
+ const newParams = {
+ ...params,
+ limit,
+ };
+ if (marker) {
+ newParams.offset = marker;
+ }
+ return request.get(url, newParams);
+ }
+ ```
+
+- `requestListAllByLimit`
+ - 当`this.fetchListByLimit=true`时,前端分页使用该方法获取所有数据
+ - 通常不需要复写
+- `updateUrl`
+ - 更新列表数据请求的 url
+ - 不常用
+- `updateParamsSortPage`
+ - 使用后端排序时,对排序参数的处理
+ - 使用后端排序时,会在资源列表代码`pages/xxx/XXX/index.jsx`中自动生成相应的请求参数,store 对这些参数往往需要再次整理,否则会不符合 API 的参数要求
+ - 以云硬盘`src/stores/cinder/volume.js`为例
+
+ ```javascript
+ updateParamsSortPage = (params, sortKey, sortOrder) => {
+ if (sortKey && sortOrder) {
+ params.sort_keys = sortKey;
+ params.sort_dirs = sortOrder === 'descend' ? 'desc' : 'asc';
+ }
+ };
+ ```
+
+- `listFilterByProject`
+ - 列表数据是否需要基于项目信息过滤
+ - `admin`权限下的部分 Openstack 资源(如`neutron`),会默认返回所有项目的数据,所以在控制台展示资源时,会根据该配置过滤数据
+ - 默认值是`false`
+ - 以 VPN`src/stores/neutron/vpn-service.js`为例
+
+ ```javascript
+ get listFilterByProject() {
+ return true;
+ }
+ ```
+
+- `fetchList`
+ - `pages`下的列表页通常使用`this.store.fetchList`来获取前端分页数据
+ - 不建议复写该函数,如果需要再加工数据,建议使用`listDidFetch`
+ - 该函数会更新`this.list`属性中的相关数据,`pages`下的资源列表组件也是基于`this.list`进行数据展示
+- `fetchListByPage`
+ - `pages`下的列表页通常使用`this.store.fetchList`来获取后端分页数据
+ - 不建议复写该函数,如果需要再加工数据,建议使用`listDidFetch`
+ - 该函数会更新`this.list`属性中的相关数据,`pages`下的资源列表组件也是基于`this.list`进行数据展示
+- `getCountForPage`
+ - 获取列表数据的总量
+ - 通常在后端分页时可复写
+- `getDetailParams`
+ - 更新详情数据请求时的参数
+ - 默认值为
+
+ ```javascript
+ getDetailParams = () => undefined;
+ ```
+
+- `fetchDetail`
+ - `pages`下的详情页通常使用`this.store.fetchDetail`来获取详情数据
+ - 通常不需要复写
+ - 数据再加工通常是重写`mapper`或`detailDidFetch`
+- `create`
+ - 创建资源
+ - 使用`POST`api
+ - 通常不需要复写
+ - 使用`this.submitting`保证在发送请求时页面处于`loading`状态
+- `edit`
+ - 更新资源
+ - 使用`PUT`api
+ - 通常不需要复写
+ - 使用`this.submitting`保证在发送请求时页面处于`loading`状态
+- `patch`
+ - 更新资源
+ - 使用`PATCH`api
+ - 通常不需要复写
+ - 使用`this.submitting`保证在发送请求时页面处于`loading`状态
+- `delete`
+ - 删除资源
+ - 使用`DELETE`api
+ - 通常不需要复写
+ - 使用`this.submitting`保证在发送请求时页面处于`loading`状态
+
+## 不需要复写的属性与函数
+
+- `submitting`
+ - 用于数据创建、数据更新时
+ - 依据请求的响应变更`this.isSubmitting`,对应的 Form,列表页等会展示 Loading 状态
+- `currentProject`
+ - 当前用户登录的项目 ID
+- `itemInCurrentProject`
+ - 数据是否属于当前用户登录的项目
+- `listDidFetchProject`
+ - 对列表数据添加项目信息
+- `requestListAll`
+ - 前端分页获取所有数据
+- `requestListByPage`
+ - 后端分页所有当前页的数据
+- `pureFetchList`
+ - 列表数据的请求函数
+ - 返回原始数据,不会对 API 的返回数据做加工
+- `parseMarker`
+ - 使用后端分页时,从返回数据中解析出`marker`,用于请求上一页、下一页数据时使用
+- `updateMarker`
+ - 更新`list`的`markers`
+ - `list.markers`是个数组,每个元素对应于`下标+1`页的`marker`
+- `getMarker`
+ - 获取指定页对应的`marker`
+- `getListDataFromResult`
+ - 从 API 的返回值中取出列表数据
+ - 利用`this.listResponseKey`获取
+- `setSelectRowKeys`
+ - 对`pages`下的资源列表组件列表中数据项的选中记录
+- `clearData`
+ - 清空`list`数据
+
+## 基类中的基础函数
+
+- 建议查看代码理解,`src/stores/base.js`
diff --git a/docs/zh/develop/3-6-FormAction-introduction.md b/docs/zh/develop/3-6-FormAction-introduction.md
new file mode 100644
index 00000000..d58f73a4
--- /dev/null
+++ b/docs/zh/develop/3-6-FormAction-introduction.md
@@ -0,0 +1,391 @@
+简体中文 | [English](/docs/en/develop/3-6-FormAction-introduction.md)
+
+# 用途
+
+![Form单页](/docs/zh/develop/images/form/page.png)
+
+- 操作按钮点击后,单页显示 Form 表单
+- 有独立的路由可供访问
+- 一般用于创建资源,或是表单内容较多的 Form
+- 点击`确认`按钮后,会根据请求的发送情况,展示`loading`状态,请求成功后,会自动跳转到相应的资源列表页
+- 点击`取消`按钮后,会自动跳转到相应的资源列表页
+- 如果请求发送成功,会在右上角展示操作成功的提示信息,该提示信息几秒后可自动消失
+
+ ![Form单页](/docs/zh/develop/images/form/create-success.png)
+
+- 如果请求发送失败,会在表单页的右上角展示错误信息,该提示信息只有点击关闭按钮后才可消失
+
+# FormAction 代码文件
+
+- `src/containers/Action/FormAction/index.jsx`
+
+# FormAction 属性与函数定义介绍
+
+- 单页表单都继承于 FormAction 组件
+- 代码位置:`pages/xxxx/containers/XXXX/actions/xxx.jsx`
+- 只需要复写部分函数即可完成页面的开发
+- 属性与函数分为以下四种,
+ - 必须复写的属性与函数,主要包含:
+ - 操作的 ID
+ - 操作的标题
+ - 页面对应的路径
+ - 资源列表页面对应的路径
+ - 操作对应的权限
+ - 对是否禁用操作的判定
+ - 表单项的配置
+ - 发送请求的函数
+ - 按需复写的函数与属性,主要包含:
+ - 表单的默认值
+ - 无需复写的函数与属性,主要包含:
+ - 当前页是否是管理平台页面
+ - 基类中的基础函数,主要包含:
+ - 渲染页面
+ - 对请求状态的展示
+ - 对请求结果的展示
+ - 更详细与全面的介绍见下
+
+## 必须复写的属性与函数
+
+- `id`
+ - 静态属性
+ - 资源操作的 ID
+ - 需要具有唯一性,只针对资源的`actions`中的所有操作具有唯一性即可
+ - 必须复写该属性
+ - 以创建云硬盘`src/pages/storage/containers/Volume/actions/Create/index.jsx`为例
+
+ ```javascript
+ static id = 'volume-create';
+ ```
+
+- `title`
+ - 静态属性
+ - 资源操作的标题
+ - 以创建云硬盘`src/pages/storage/containers/Volume/actions/Create/index.jsx`为例
+
+ ```javascript
+ static title = t('Create Volume');
+ ```
+
+- `path`
+ - 资源操作的对应的路由
+ - 静态属性或静态函数
+ - 静态函数时,参数为
+ - 参数`item`,资源列表中的条目数据
+ - 参数`containerProps`,父级 container(即按钮所在资源列表页面)的`props`属性
+ - 以创建镜像`src/pages/compute/containers/Image/actions/Create.jsx`为例
+ - 管理平台访问的路径是`/compute/image-admin/create`
+ - 控制台访问的路径是`/compute/image/create`
+
+ ```javascript
+ static path = (_, containerProp) => {
+ const { isAdminPage } = containerProp;
+ return isAdminPage
+ ? '/compute/image-admin/create'
+ : '/compute/image/create';
+ };
+ ```
+
+ - 静态属性,以创建云硬盘`src/pages/storage/containers/Volume/actions/Create/index.jsx`为例
+
+ ```javascript
+ static path = '/storage/volume/create';
+ ```
+
+- `policy`
+ - 静态属性
+ - 页面对应的权限,如果权限验证不通过,则不会在资源列表页面显示该操作按钮
+ - 以云硬盘`src/pages/storage/containers/Volume/actions/Create/index.jsx`为例
+
+ ```javascript
+ static policy = 'volume:create';
+ ```
+
+- `allowed`
+ - 静态函数
+ - 判定操作是否需要被禁用
+ - 返回`Promise`
+ - 不需用禁用的按钮,直接写作
+
+ ```javascript
+ static allowed() {
+ return Promise.resolve(true);
+ }
+ ```
+
+ - 参数`item`,资源列表中的条目数据,一般用在资源列表中的条目的操作判定
+ - 参数`containerProps`,父级 container(即按钮所在资源列表页面)的`props`属性,一般用在详情页下相关资源的操作判定
+ - 以创建用户`src/pages/identity/containers/User/actions/Create.jsx`为例
+ - 如果是域详情中的用户列表,则不展示创建用户按钮
+
+ ```javascript
+ static allowed(item, containerProps) {
+ const {
+ match: { path },
+ } = containerProps;
+ if (path.indexOf('domain-admin/detail') >= 0) {
+ return Promise.resolve(false);
+ }
+ return Promise.resolve(true);
+ }
+ ```
+
+- `name`
+ - 该操作对应的名称
+ - 在请求后提示语中使用该名称
+ - 以云硬盘`src/pages/storage/containers/Volume/actions/Create/index.jsx`为例
+
+ ```javascript
+ get name() {
+ return t('create volume');
+ }
+ ```
+
+- `listUrl`
+ - 该操作对应的资源列表页
+ - 操作请求成功后,会自动进入到资源列表页
+ - 以云硬盘`src/pages/storage/containers/Volume/actions/Create/index.jsx`为例
+
+ ```javascript
+ get listUrl() {
+ return this.getUrl('/storage/volume');
+ }
+ ```
+
+- `formItems`
+ - 该操作表单对应的表单项配置列表
+ - 每个表单项的配置信息可参考[3-10-FormItem 介绍](3-10-FormItem-introduction.md)
+ - 以创建域`src/pages/identity/containers/Domain/actions/Create.jsx`为例
+ - 表单包含名称、描述、状态
+
+ ```javascript
+ get formItems() {
+ return [
+ {
+ name: 'name',
+ label: t('Name'),
+ type: 'input',
+ placeholder: t('Please input name'),
+ required: true,
+ help: t('The name cannot be modified after creation'),
+ },
+ {
+ name: 'description',
+ label: t('Description'),
+ type: 'textarea',
+ },
+ {
+ name: 'enabled',
+ label: t('Status'),
+ type: 'radio',
+ optionType: 'default',
+ options: statusTypes,
+ required: true,
+ isWrappedValue: true,
+ help: t(
+ 'Forbidden the domain will have a negative impact, all project and user in domain will be forbidden'
+ ),
+ },
+ ];
+ }
+ ```
+
+- `onSubmit`
+ - 该操作的请求函数
+ - 操作请求成功后,会自动进入到资源列表页
+ - 操作失败后,会在表单页显示错误提示
+ - 返回`Promise`
+ - 返回表单对应的`store`中的函数
+ - 以创建域`src/pages/identity/containers/Domain/actions/Create.jsx`为例
+
+ ```javascript
+ onSubmit = (values) => {
+ values.enabled = values.enabled.value;
+ return this.store.create(values);
+ };
+ ```
+
+## 按需复写的属性与函数
+
+- `init`
+ - 初始化操作
+ - 在其中定义`this.store`,`loading`状态的展示是基于`this.store.isSubmitting`
+ - 在其中调用获取表单所需其他数据的函数
+ - 对`this.state`中属性的更新
+ - 以创建云硬盘`src/pages/storage/containers/Volume/actions/Create/index.jsx`为例
+ - 获取配额信息、可用域数据、镜像数据、云硬盘类型
+ - 更新`this.state`中的初始值
+
+ ```javascript
+ init() {
+ this.snapshotStore = globalSnapshotStore;
+ this.imageStore = globalImageStore;
+ this.volumeStore = globalVolumeStore;
+ this.volumeTypeStore = globalVolumeTypeStore;
+ this.backupstore = globalBackupStore;
+ this.getQuota();
+ this.getAvailZones();
+ this.getImages();
+ this.getVolumeTypes();
+ this.state = {
+ ...this.state,
+ count: 1,
+ sharedDisabled: false,
+ };
+ }
+ ```
+
+- `defaultValue`
+ - 表单的初始值
+ - 默认值是`{}`
+ - 以创建云硬盘`src/pages/storage/containers/Volume/actions/Create/index.jsx`为例
+ - 设置表单的默认源、大小、项目、可用域、云硬盘类型
+
+ ```javascript
+ get defaultValue() {
+ const size = this.quotaIsLimit && this.maxSize < 10 ? this.maxSize : 10;
+ const { initVolumeType } = this.state;
+ const values = {
+ source: this.sourceTypes[0],
+ size,
+ project: this.currentProjectName,
+ availableZone: (this.availableZones[0] || []).value,
+ volume_type: initVolumeType,
+ };
+ return values;
+ }
+ ```
+
+- `nameForStateUpdate`
+ - 表单项内容变动时,更新到`this.state`中的表单键值对
+ - 这些存储到`this.store`中的键值对往往会影响表单项的展示,需要配合`get formItems`中的代码使用
+ - 如展开、隐藏更多配置项
+ - 如某些表单项必填性的变动
+ - 默认对`radio`, `more`类型的表单项的变动保存到`this.state`中
+ - 以创建云硬盘`src/pages/storage/containers/Volume/actions/Create/index.jsx`为例
+ - 当`source=image`时,展示镜像选择列表,并基于镜像的选择,设置云硬盘容量的最小值
+ - 当`source=snapshot`时,展示云硬盘快照列表,并基于镜像的选择,设置云硬盘容量的最小值
+
+ ```javascript
+ get nameForStateUpdate() {
+ return ['source', 'image', 'snapshot'];
+ }
+ ```
+
+- `renderFooterLeft`
+ - 对表单底部左侧内部的渲染
+ - 默认返回`null`
+ - 以创建云硬盘`src/pages/storage/containers/Volume/actions/Create/index.jsx`为例
+ - 展示批量创建的数量
+ - 基于输入的数量与剩余配额判定当前表单是否正确
+
+ ```javascript
+ const { count = 1 } = this.state;
+ const configs = {
+ min: 1,
+ max: 100,
+ precision: 0,
+ onChange: this.onCountChange,
+ formatter: (value) => `$ ${value}`.replace(/\D/g, ''),
+ };
+ return (
+
+ {t('Count')}
+
+ {this.renderBadge()}
+
+ );
+ ```
+
+- `errorText`
+ - 错误信息的展示
+ - 一般不需要复写
+ - 以创建云硬盘`src/pages/storage/containers/Volume/actions/Create/index.jsx`为例
+ - 展示了配额验证不通过的错误信息,当配额验证不通过时,并不发送请求,而是直接展示了错误提示信息
+
+ ```javascript
+ get errorText() {
+ const { status } = this.state;
+ if (status === 'error') {
+ return t(
+ 'Unable to create volume: insufficient quota to create resources.'
+ );
+ }
+ return super.errorText;
+ }
+ ```
+
+- `instanceName`
+ - 请求发送后,提示信息中的资源名称
+ - 默认值为`this.values.name`
+ - 以创建云硬盘`src/pages/storage/containers/Volume/actions/Create/index.jsx`为例
+ - 如果是批量创建云硬盘,则按`${name}-${index + 1}`的形式展示名称
+
+ ```javascript
+ get instanceName() {
+ const { name } = this.values || {};
+ const { count = 1 } = this.state;
+ if (count === 1) {
+ return name;
+ }
+ return new Array(count)
+ .fill(count)
+ .map((_, index) => `${name}-${index + 1}`)
+ .join(', ');
+ }
+ ```
+
+- `labelCol`
+ - 配置表单左侧标签的布局
+ - 默认值为
+
+ ```javascript
+ get labelCol() {
+ return {
+ xs: { span: 5 },
+ sm: { span: 3 },
+ };
+ }
+ ```
+
+ - 以创建镜像`src/pages/compute/containers/Image/actions/Create.jsx`为例
+
+ ```javascript
+ get labelCol() {
+ return {
+ xs: { span: 6 },
+ sm: { span: 5 },
+ };
+ }
+ ```
+
+- `wrapperCol`
+ - 配置表单右侧内容的布局
+ - 默认值为
+
+ ```javascript
+ get wrapperCol() {
+ return {
+ xs: { span: 10 },
+ sm: { span: 8 },
+ };
+ }
+ ```
+
+## 不需要复写的属性与函数
+
+- `values`
+ - 表单验证成功后,更新的表单值
+- `isAdminPage`
+ - 当前页面是否是“管理平台”的页面
+- `getUrl`
+ - 生成页面 Url 的函数
+ - 如:需要给关联资源提供跳转功能,使用该函数,可以在控制台跳转到控制台的相应地址,在管理平台跳转到管理平台的相应地址
+
+## 基类中的基础函数
+
+- `FormAction`继承于`BaseForm`
+- 建议查看代码理解,`src/components/Form/index.jsx`
diff --git a/docs/zh/develop/3-7-ModalAction-introduction.md b/docs/zh/develop/3-7-ModalAction-introduction.md
new file mode 100644
index 00000000..fdb95c92
--- /dev/null
+++ b/docs/zh/develop/3-7-ModalAction-introduction.md
@@ -0,0 +1,387 @@
+简体中文 | [English](/docs/en/develop/3-7-ModalAction-introduction.md)
+
+# 用途
+
+![弹窗型表单](/docs/zh/develop/images/form/modal.png)
+
+- 操作按钮点击后,弹窗显示表单
+- 点击`确认`按钮后,会根据请求的发送情况,展示`loading`状态
+- 点击`取消`按钮后,弹窗消失
+- 如果请求发送成功,会在右上角展示操作成功的提示信息,该提示信息几秒后可自动消失
+- 如果请求发送失败,会在表单页的右上角展示错误信息,该提示信息只有点击关闭按钮后才可消失
+- 支持批量操作,在表格中选中多个条目后,可点击表格上方的操作按钮,进行批量操作
+
+# ModalAction 代码文件
+
+- `src/containers/Action/ModalAction/index.jsx`
+
+# ModalAction 属性与函数定义介绍
+
+- 弹窗型表单都继承于 ModalAction 组件
+- 代码位置:`pages/xxxx/containers/XXXX/actions/xxx.jsx`
+- 对于表单内容比较少的情况,通常是使用弹窗型的表单形式
+- 只需要复写部分函数即可完成页面的开发
+- 属性与函数分为以下四种,
+ - 必须复写的属性与函数,主要包含:
+ - 操作的 ID
+ - 操作的标题
+ - 操作对应的权限
+ - 对是否禁用操作的判定
+ - 表单项的配置
+ - 发送请求的函数
+ - 按需复写的函数与属性,主要包含:
+ - 表单的默认值
+ - 表单的尺寸
+ - 表单中右侧标题与左侧表单主题的布局
+ - 是否是异步操作
+ - 资源的名称
+ - 请求结果提示语中是否要展示资源名称
+ - 操作按钮上的文字
+ - 无需复写的函数与属性,主要包含:
+ - 当前页是否是管理平台页面
+ - 基类中的基础函数,主要包含:
+ - 渲染页面
+ - 对请求状态的展示
+ - 对请求结果的展示
+ - 更详细与全面的介绍见下
+
+## 必须复写的属性与函数
+
+- `id`
+ - 静态属性
+ - 资源操作的 ID
+ - 需要具有唯一性,只针对资源的`actions`中的所有操作具有唯一性即可
+ - 必须复写该属性
+ - 以挂载云硬盘`src/pages/compute/containers/Instance/actions/AttachVolume.jsx`为例
+
+ ```javascript
+ static id = 'attach-volume';
+ ```
+
+- `title`
+ - 静态属性
+ - 资源操作的标题
+ - 以挂载云硬盘`src/pages/compute/containers/Instance/actions/AttachVolume.jsx`为例
+
+ ```javascript
+ static title = t('Attach Volume');
+ ```
+
+- `name`
+ - 该操作对应的名称
+ - 在请求后提示语中使用该名称
+ - 以挂载云硬盘`src/pages/compute/containers/Instance/actions/AttachVolume.jsx`为例
+
+ ```javascript
+ get name() {
+ return t('Attach volume');
+ }
+ ```
+
+- `policy`
+ - 静态属性
+ - 操作对应的权限,如果权限验证不通过,则不会在资源列表页面显示该操作按钮
+ - 以挂载云硬盘`src/pages/compute/containers/Instance/actions/AttachVolume.jsx`为例
+
+ ```javascript
+ static policy = 'os_compute_api:os-volumes-attachments:create';
+ ```
+
+- `allowed`
+ - 静态函数
+ - 判定操作是否需要被禁用
+ - 返回`Promise`
+ - 不需用禁用的按钮,直接写作
+
+ ```javascript
+ static allowed() {
+ return Promise.resolve(true);
+ }
+ ```
+
+ - 参数`item`,资源列表中的条目数据,一般用在资源列表中的条目的操作判定
+ - 参数`containerProps`,父级 container(即按钮所在资源列表页面)的`props`属性,一般用在详情页下相关资源的操作判定
+ - 以挂载云硬盘`src/pages/compute/containers/Instance/actions/AttachVolume.jsx`为例
+ - 管理平台不展示该操作按钮
+ - 当云主机满足:运行中、不处于删除中、未锁定、不是裸机 时,才会展示按操作按钮
+
+ ```javascript
+ static allowed = (item, containerProps) => {
+ const { isAdminPage } = containerProps;
+ return Promise.resolve(
+ !isAdminPage &&
+ isActive(item) &&
+ isNotDeleting(item) &&
+ isNotLocked(item) &&
+ !isIronicInstance(item)
+ );
+ };
+ ```
+
+- `formItems`
+ - 该操作表单对应的表单项配置列表
+ - 每个表单项的配置信息可参考[3-10-FormItem 介绍](3-10-FormItem-introduction.md)
+ - 以挂载云硬盘`src/pages/compute/containers/Instance/actions/AttachVolume.jsx`为例
+ - 表单项包含:云主机的名称展示、云硬盘的选择
+
+ ```javascript
+ get formItems() {
+ return [
+ {
+ name: 'instance',
+ label: t('Instance'),
+ type: 'label',
+ iconType: 'instance',
+ },
+ {
+ name: 'volume',
+ label: t('Volume'),
+ type: 'volume-select-table',
+ tip: multiTip,
+ isMulti: false,
+ required: true,
+ serverId: this.item.id,
+ disabledFunc: (record) => {
+ const diskFormat = _get(
+ record,
+ 'origin_data.volume_image_metadata.disk_format'
+ );
+ return diskFormat === 'iso';
+ },
+ },
+ ];
+ }
+ ```
+
+- `onSubmit`
+ - 该操作的请求函数
+ - 操作请求成功后,弹窗会消失,并显示成功提示,几秒后提示会消失
+ - 操作失败后,弹窗会消失,并显示错误提示,需要手动关闭提示,提示才会消失
+ - 返回`Promise`
+ - 返回表单对应的`store`中的函数
+ - 以挂载云硬盘`src/pages/compute/containers/Instance/actions/AttachVolume.jsx`为例
+
+ ```javascript
+ onSubmit = (values) => {
+ const { volume } = values;
+ const { id } = this.item;
+ const volumeId = volume.selectedRowKeys[0];
+ const body = {
+ volumeAttachment: {
+ volumeId,
+ },
+ };
+ return this.store.attachVolume({ id, body });
+ };
+ ```
+
+## 按需复写的属性与函数
+
+- `init`
+ - 初始化操作
+ - 在其中定义`this.store`,`loading`状态的展示是基于`this.store.isSubmitting`
+ - 在其中调用获取表单所需其他数据的函数
+ - 对`this.state`中属性的更新
+ - 以挂载云硬盘`src/pages/compute/containers/Instance/actions/AttachVolume.jsx`为例
+ - 定义了操作对应的`store`
+
+ ```javascript
+ init() {
+ this.store = globalServerStore;
+ }
+ ```
+
+- `defaultValue`
+ - 表单的初始值
+ - 默认值是`{}`
+ - 以挂载云硬盘`src/pages/compute/containers/Instance/actions/AttachVolume.jsx`为例
+ - 设置了表单中,云主机名称的初始值
+
+ ```javascript
+ get defaultValue() {
+ const { name } = this.item;
+ const value = {
+ instance: name,
+ };
+ return value;
+ }
+ ```
+
+- `nameForStateUpdate`
+ - 表单项内容变动时,更新到`this.state`中的表单键值对
+ - 这些存储到`this.store`中的键值对往往会影响表单项的展示,需要配合`get formItems`中的代码使用
+ - 如展开、隐藏更多配置项
+ - 如某些表单项必填性的变动
+ - 默认对`radio`, `more`类型的表单项的变动保存到`this.state`中
+ - 以云主机挂载网卡`src/pages/compute/containers/Instance/actions/AttachInterface.jsx`为例
+ - 表单中的网络的选中变更后,会更新子网列表的内容
+ - 但表单中子网的选中变更后,会更新输入 IP 的判定等
+
+ ```javascript
+ get nameForStateUpdate() {
+ return ['network', 'ipType', 'subnet'];
+ }
+ ```
+
+- `instanceName`
+ - 请求发送后,提示信息中的资源名称
+ - 默认值为`this.values.name`
+ - 以编辑浮动 IP`src/pages/network/containers/FloatingIp/actions/Edit.jsx`为例
+ - 提示的名称是浮动 IP 的地址
+
+ ```javascript
+ get instanceName() {
+ return this.item.floating_ip_address;
+ }
+ ```
+
+- `isAsyncAction`
+ - 当前操作是否是异步操作
+ - 默认是`false`
+ - 如果是异步操作,提示语为:`xxx指令已下发,实例名称:xxx 您可等待几秒关注列表数据的变更或是手动刷新数据,以获取最终展示结果。`
+ - 如果是同步操作,提示语为:`xxx成功,实例名称:xxx。`
+ - 以挂载云硬盘`src/pages/compute/containers/Instance/actions/AttachVolume.jsx`为例
+
+ ```javascript
+ get isAsyncAction() {
+ return true;
+ }
+ ```
+
+- `messageHasItemName`
+ - 请求结果的提示语中,是否要包含实例名称
+ - 默认值为`true`
+ - 有些资源,不存在名称,则可设置该值为`false`
+ - 以裸机节点创建端口``为例
+
+ ```javascript
+ get messageHasItemName() {
+ return false;
+ }
+ ```
+
+- `buttonText`
+ - 静态属性
+ - 当操作按钮上的文字与弹窗的标题不一致时,需要复用该属性
+ - 以编辑镜像`src/pages/compute/containers/Image/actions/Edit.jsx`为例
+
+ ```javascript
+ static buttonText = t('Edit');
+ ```
+
+- `buttonType`
+ - 静态属性
+ - 按钮的类型,支持`primary`、`danger`
+ - 当按钮要强调操作危险性时,按钮或按钮上的文字一般为红色,使用`danger`
+ - 以禁止 Cinder 服务`src/pages/configuration/containers/SystemInfo/CinderService/actions/Disable.jsx`为例
+
+ ```javascript
+ static buttonType = 'danger';
+ ```
+
+- `modalSize`
+ - 静态函数
+ - 标识弹出框的宽度:值为`small`、`middle`、`large`
+ - 值与宽度的对应为:
+ - `small`: 520
+ - `middle`: 720
+ - `large`: 1200
+ - 与`getModalSize`配合使用
+ - 默认值为`small`,即弹窗的宽度是 520px
+
+ ```javascript
+ static get modalSize() {
+ return 'small';
+ }
+ ```
+
+ - 以挂载云硬盘`src/pages/compute/containers/Instance/actions/AttachVolume.jsx`为例
+ - 表单的大小是`large`
+
+ ```javascript
+ static get modalSize() {
+ return 'large';
+ }
+
+ getModalSize() {
+ return 'large';
+ }
+ ```
+
+- `getModalSize`
+ - 配置表单左侧标题的布局
+ - 值为`small`、`middle`、`large`
+- `labelCol`
+ - 配置表单左侧标签的布局
+ - 默认值为
+
+ ```javascript
+ get labelCol() {
+ const size = this.getModalSize();
+ if (size === 'large') {
+ return {
+ xs: { span: 6 },
+ sm: { span: 4 },
+ };
+ }
+ return {
+ xs: { span: 8 },
+ sm: { span: 6 },
+ };
+ }
+ ```
+
+ - 以编辑域`src/pages/identity/containers/Domain/actions/Edit.jsx`为例
+
+ ```javascript
+ get labelCol() {
+ return {
+ xs: { span: 6 },
+ sm: { span: 5 },
+ };
+ }
+ ```
+
+- `wrapperCol`
+ - 配置表单右侧内容的布局
+ - 默认值为
+
+ ```javascript
+ get wrapperCol() {
+ return {
+ xs: { span: 16 },
+ sm: { span: 16 },
+ };
+ }
+ ```
+
+ - 以管理云主机类型元数据`src/pages/compute/containers/Flavor/actions/ManageMetadata.jsx`为例
+
+ ```javascript
+ get wrapperCol() {
+ return {
+ xs: { span: 18 },
+ sm: { span: 20 },
+ };
+ }
+ ```
+
+## 不需要复写的属性与函数
+
+- `isAdminPage`
+ - 当前页面是否是“管理平台”的页面
+- `successText`
+ - 请求后生成的成功提示语
+- `errorText`
+ - 请求失败后生成的报错提示语
+- `containerProps`
+ - 获取来源于按钮所在父级组件的`props`
+- `item`
+ - 获取操作对应的数据
+- `items`
+ - 获取批量操作对应的数据
+
+## 基类中的基础函数
+
+- `ModalAction`继承于`BaseForm`
+- 建议查看代码理解,`src/components/Form/index.jsx`
diff --git a/docs/zh/develop/3-8-ConfirmAction-introduction.md b/docs/zh/develop/3-8-ConfirmAction-introduction.md
new file mode 100644
index 00000000..435992e9
--- /dev/null
+++ b/docs/zh/develop/3-8-ConfirmAction-introduction.md
@@ -0,0 +1,257 @@
+简体中文 | [English](/docs/en/develop/3-8-ConfirmAction-introduction.md)
+
+# 用途
+
+![确认型](/docs/zh/develop/images/form/confirm.png)
+
+- 操作按钮点击后,显示确认类型的表单
+- 点击`确认`按钮后,会根据请求的发送情况,展示`loading`状态
+- 点击`取消`按钮后,弹窗消失
+- 如果请求发送成功,会在右上角展示操作成功的提示信息,该提示信息几秒后可自动消失
+- 如果请求发送失败,会在表单页的右上角展示错误信息,该提示信息只有点击关闭按钮后才可消失
+- 支持批量操作,在表格中选中多个条目后,可点击表格上方的操作按钮,进行批量操作
+- 使用批量操作时,会对批量选中的资源中不符合操作条件的资源做出提示
+
+# ConfirmAction 代码文件
+
+- `src/containers/Action/ConfirmAction/index.jsx`
+
+# ModalAction 属性与函数定义介绍
+
+- 弹窗型表单都继承于 ModalAction 组件
+- 代码位置:`pages/xxxx/containers/XXXX/actions/xxx.jsx`
+- 某些操作,只需要再次确认,无需用户输入更多内容即可,此时可使用该类型的组件,如:关闭云主机
+- 只需要复写部分函数即可完成页面的开发
+- 属性与函数分为以下四种,
+ - 必须复写的属性与函数,主要包含:
+ - 操作的 ID
+ - 操作的标题
+ - 操作对应的权限
+ - 对是否禁用操作的判定
+ - 发送请求的函数
+ - 按需复写的函数与属性,主要包含:
+ - 资源的名称
+ - 请求结果提示语中是否要展示资源名称
+ - 是否是异步操作
+ - 操作按钮上的文字
+ - 无需复写的函数与属性,主要包含:
+ - 当前页是否是管理平台页面
+ - 基类中的基础函数,主要包含:
+ - 渲染页面
+ - 对请求状态的展示
+ - 对请求结果的展示
+ - 更详细与全面的介绍见下
+
+## 必须复写的属性与函数
+
+- `id`
+ - 资源操作的 ID
+ - 需要具有唯一性,只针对资源的`actions`中的所有操作具有唯一性即可
+ - 必须复写该属性
+ - 以停止云主机`src/pages/compute/containers/Instance/actions/Stop.jsx`为例
+
+ ```javascript
+ get id() {
+ return 'stop';
+ }
+ ```
+
+- `title`
+ - 资源操作的标题
+ - 以停止云主机`src/pages/compute/containers/Instance/actions/Stop.jsx`为例
+
+ ```javascript
+ get title() {
+ return t('Stop Instance');
+ }
+ ```
+
+- `actionName`
+ - 该操作对应的名称
+ - 在请求后提示语中使用该名称
+ - 以停止云主机`src/pages/compute/containers/Instance/actions/Stop.jsx`为例
+
+ ```javascript
+ get actionName() {
+ return t('stop instance');
+ }
+ ```
+
+- `policy`
+ - 页面对应的权限,如果权限验证不通过,则不会在资源列表页面显示该操作按钮
+ - 以停止云主机`src/pages/compute/containers/Instance/actions/Stop.jsx`为例
+
+ ```javascript
+ policy = 'os_compute_api:servers:stop';
+ ```
+
+- `allowedCheckFunc`
+ - 判定操作是否需要被禁用
+ - 返回`Boolean`
+ - 不需用禁用的按钮,直接写作
+
+ ```javascript
+ allowedCheckFunc = () => true;
+ ```
+
+ - 参数`item`,操作对应的数据
+ - 以停止云主机`src/pages/compute/containers/Instance/actions/Stop.jsx`为例
+ - 当云主机满足以下条件才会显示该操作按钮:处于运行中状态,控制台非锁定或是在管理平台
+
+ ```javascript
+ allowedCheckFunc = (item) => {
+ if (!item) {
+ return true;
+ }
+ return isNotLockedOrAdmin(item, this.isAdminPage) && this.isRunning(item);
+ };
+ ```
+
+- `onSubmit`
+ - 该操作的请求函数
+ - 操作请求成功后,弹窗会消失,并显示成功提示,几秒后提示会消失
+ - 操作失败后,弹窗会消失,并显示错误提示,需要手动关闭提示,提示才会消失
+ - 返回`Promise`
+ - 返回表单对应的`store`中的函数
+ - 以停止云主机`src/pages/compute/containers/Instance/actions/Stop.jsx`为例
+
+ ```javascript
+ onSubmit = (item) => {
+ const { id } = item || this.item;
+ return globalServerStore.stop({ id });
+ };
+ ```
+
+## 按需复写的属性与函数
+
+- `buttonText`
+ - 当操作按钮上的文字与弹窗的标题不一致时,需要复用该属性
+ - 以停止云主机`src/pages/compute/containers/Instance/actions/Stop.jsx`为例
+ - 弹窗上的标题是“停止云主机”,按钮上的文字是“停止”
+
+ ```javascript
+ get buttonText() {
+ return t('Stop');
+ }
+ ```
+
+- `buttonType`
+ - 按钮的类型,支持`primary`、`danger`、`default`
+ - 默认值为`default`
+ - 当按钮要强调操作危险性时,按钮或按钮上的文字一般为红色,使用`danger`
+ - 以停止云主机`src/pages/compute/containers/Instance/actions/Stop.jsx`为例
+
+ ```javascript
+ get buttonType() {
+ return 'danger';
+ }
+ ```
+
+- `passiveAction`
+ - 批量操作时,如果某个资源不符合条件,会在发送请求前展示提示语,如果提示语需要以被动语态,则需要设置该属性
+ - 以停止云主机`src/pages/compute/containers/Instance/actions/Stop.jsx`为例
+
+ ```javascript
+ get passiveAction() {
+ return t('be stopped');
+ }
+ ```
+
+- `isAsyncAction`
+ - 当前操作是否是异步操作
+ - 默认是`false`
+ - 如果是异步操作,提示语为:`xxx指令已下发,实例名称:xxx 您可等待几秒关注列表数据的变更或是手动刷新数据,以获取最终展示结果。`
+ - 如果是同步操作,提示语为:`xxx成功,实例名称:xxx。`
+ - 以停止云主机`src/pages/compute/containers/Instance/actions/Stop.jsx`为例
+
+ ```javascript
+ get isAsyncAction() {
+ return true;
+ }
+ ```
+
+- `messageHasItemName`
+ - 请求结果的提示语中,是否要包含实例名称
+ - 默认值为`true`
+ - 有些资源,不存在名称,则可设置该值为`false`
+- `performErrorMsg`
+ - 批量操作时,如果某个资源不符合条件,会在发送请求前展示提示语
+ - 默认值为`无法xxx, 实例名称:xxxx。`
+ - 以停止云主机`src/pages/compute/containers/Instance/actions/Stop.jsx`为例
+ - 如果选中的云主机不处于运行中状态,提示`云主机\"{ name }\"状态不是运行中,无法关闭。`
+ - 如果选中的云主机有处于锁定状态的,提示`云主机\"{ name }\"被锁定,无法关闭。`
+ - 其他情况,皆提示`无法关闭云主机\"{ name }\"`
+
+ ```javascript
+ performErrorMsg = (failedItems) => {
+ const instance = isArray(failedItems) ? failedItems[0] : failedItems;
+ let errorMsg = t('You are not allowed to stop instance "{ name }".', {
+ name: instance.name,
+ });
+ if (!this.isRunning(instance)) {
+ errorMsg = t(
+ 'Instance "{ name }" status is not in active or suspended, can not stop it.',
+ { name: instance.name }
+ );
+ } else if (!isNotLockedOrAdmin(instance, this.isAdminPage)) {
+ errorMsg = t('Instance "{ name }" is locked, can not stop it.', {
+ name: instance.name,
+ });
+ }
+ return errorMsg;
+ };
+ ```
+
+- `getNameOne`
+ - 提示语中实例名称的来源
+ - 默认是
+
+ ```javascript
+ getNameOne = (data) => data.name;`
+ ```
+
+ - 参数`data`为操作对应的资源数据
+ - 以释放浮动 IP`src/pages/network/containers/FloatingIp/actions/Release.jsx`为例
+
+ ```javascript
+ getNameOne = (data) => data.floating_ip_address;
+ ```
+
+- `getName`
+ - 不建议复写该函数
+ - 建议复写`getNameOne`
+- `confirmContext`
+ - 确认弹窗中的提示语
+ - 默认为`确认{ action }(实例名称:{name})?`
+ - 以删除云主机类型`src/pages/compute/containers/Flavor/actions/Delete.jsx`为例
+ - 提示`若有云主机正在使用此 flavor,删除会导致云主机的 flavor 数据缺失,确定删除 {name} ?`
+
+ ```javascript
+ confirmContext = (data) => {
+ const name = this.getName(data);
+ return t(
+ "If an instance is using this flavor, deleting it will cause the instance's flavor data to be missing. Are you sure to delete {name}?",
+ { name }
+ );
+ };
+ ```
+
+- `submitErrorMsg`
+ - 操作失败后的错误提示语
+ - 一般不需要复写
+ - 默认为`无法{action},实例名称:{name}。`
+
+## 不需要复写的属性与函数
+
+- `isAdminPage`
+ - 当前页面是否是“管理平台”的页面
+- `submitSuccessMsg`
+ - 请求后生成的成功提示语
+- `submitErrorMsgBatch`
+ - 批量操作请求后生成的报错提示语
+- `perform`
+ - 批量操作时,判定选中的数据是否可操作,如果不可操作,给出相应提示语
+
+## 基类中的基础函数
+
+- 建议查看代码理解,`src/containers/Action/ConfirmAction/index.jsx`
diff --git a/docs/zh/develop/3-9-StepAction-introduction.md b/docs/zh/develop/3-9-StepAction-introduction.md
new file mode 100644
index 00000000..6589a865
--- /dev/null
+++ b/docs/zh/develop/3-9-StepAction-introduction.md
@@ -0,0 +1,293 @@
+简体中文 | [English](/docs/en/develop/3-9-StepAction-introduction.md)
+
+# 用途
+
+![分步Form](/docs/zh/develop/images/form/step.png)
+
+- 操作按钮点击后,单页显示分步操作的表单
+- 有独立的路由可供访问
+- 一般用于创建资源,或是表单内容较多的 Form
+- 支持点击`下一步`、`上一步`操作按钮
+- 点击`取消`按钮后,会自动跳转到相应的资源列表页
+- 如果请求发送成功,会在右上角展示操作成功的提示信息,该提示信息几秒后可自动消失
+
+ ![Form单页](/docs/zh/develop/images/form/create-success.png)
+
+- 如果请求发送失败,会在表单页的右上角展示错误信息,该提示信息只有点击关闭按钮后才可消失
+
+# StepAction 代码文件
+
+- `src/containers/Action/StepAction/index.jsx`
+
+# StepAction 属性与函数定义介绍
+
+- 分步表单都继承于 StepAction 组件
+- 代码位置:`pages/xxxx/containers/XXXX/actions/xxx/index.jsx`
+- 只需要复写部分函数即可完成页面的开发
+- 需要编写每一步的 Form
+- 属性与函数分为以下四种,
+ - 必须复写的属性与函数,主要包含:
+ - 操作的 ID
+ - 操作的标题
+ - 页面对应的路径
+ - 资源列表页面对应的路径
+ - 操作对应的权限
+ - 对是否禁用操作的判定
+ - 表单项的配置
+ - 发送请求的函数
+ - 每步操作的配置
+ - 按需复写的函数与属性,主要包含:
+ - 是否具有确认信息的页面
+ - 请求成功后的提示语
+ - 请求失败的报错提示语
+ - 对页面底部左侧数据的渲染
+ - 无需复写的函数与属性,主要包含:
+ - 当前页是否是管理平台页面
+ - 基类中的基础函数,主要包含:
+ - 渲染页面
+ - 对请求状态的展示
+ - 对请求结果的展示
+ - 更详细与全面的介绍见下
+
+## 必须复写的属性与函数
+
+- `id`
+ - 静态属性
+ - 资源操作的 ID
+ - 需要具有唯一性,只针对资源的`actions`中的所有操作具有唯一性即可
+ - 必须复写该属性
+ - 以创建云主机`src/pages/compute/containers/Instance/actions/StepCreate/index.jsx`为例
+
+ ```javascript
+ static id = 'instance-create';
+ ```
+
+- `title`
+ - 静态属性
+ - 资源操作的标题
+ - 以创建云主机`src/pages/compute/containers/Instance/actions/StepCreate/index.jsx`为例
+
+ ```javascript
+ static title = t('Create Instance');
+ ```
+
+- `path`
+ - 资源操作的对应的路由
+ - 静态属性或静态函数
+ - 静态函数时,参数为
+ - 参数`item`,资源列表中的条目数据
+ - 参数`containerProps`,父级 container(即按钮所在资源列表页面)的`props`属性
+ - 以创建云主机`src/pages/compute/containers/Instance/actions/StepCreate/index.jsx`为例
+ - 在云主机列表页点击创建云主机按钮,页面跳转到`/compute/instance/create`
+ - 在云主机组详情页中点击创建云主机按钮,页面跳转到`/compute/instance/create?servergroup=${detail.id}`
+
+ ```javascript
+ static path = (_, containerProps) => {
+ const { detail, match } = containerProps || {};
+ if (!detail || isEmpty(detail)) {
+ return '/compute/instance/create';
+ }
+ if (match.path.indexOf('/compute/server') >= 0) {
+ return `/compute/instance/create?servergroup=${detail.id}`;
+ }
+ };
+ ```
+
+ - 静态属性,以创建云主机类型`src/pages/compute/containers/Flavor/actions/StepCreate/index.jsx`为例
+
+ ```javascript
+ static path = '/compute/flavor-admin/create';
+ ```
+
+- `policy`
+ - 静态属性
+ - 页面对应的权限,如果权限验证不通过,则不会在资源列表页面显示该操作按钮
+ - 以创建云主机`src/pages/compute/containers/Instance/actions/StepCreate/index.jsx`为例
+
+ ```javascript
+ static policy = [
+ 'os_compute_api:servers:create',
+ 'os_compute_api:os-availability-zone:list',
+ ];
+ ```
+
+- `allowed`
+ - 静态函数
+ - 判定操作是否需要被禁用
+ - 返回`Promise`
+ - 不需用禁用的按钮,直接写作
+
+ ```javascript
+ static allowed() {
+ return Promise.resolve(true);
+ }
+ ```
+
+- `name`
+ - 该操作对应的名称
+ - 在请求后提示语中使用该名称
+ - 以创建云主机`src/pages/compute/containers/Instance/actions/StepCreate/index.jsx`为例
+
+ ```javascript
+ get name() {
+ return t('Create instance');
+ }
+ ```
+
+- `listUrl`
+ - 该操作对应的资源列表页
+ - 操作请求成功后,会自动进入到资源列表页
+ - 以创建云主机`src/pages/compute/containers/Instance/actions/StepCreate/index.jsx`为例
+ - 在镜像列表页的条目操作中,点击创建云主机并操作成功后,返回到镜像列表页
+ - 在云硬盘列表页的条目操作中,点击创建云主机并操作成功后,返回到云硬盘列表页
+ - 在云硬盘列表页的条目操作中,点击创建云主机并操作成功后,返回到云硬盘列表页
+ - 在云主机组详情页,点击创建云主机并操作成功后,返回到云主机详情页中
+ - 在云主机列表页中,点击创建云主机并操作成功后,返回到云主机列表页
+
+ ```javascript
+ get listUrl() {
+ const { image, volume, servergroup } = this.locationParams;
+ if (image) {
+ return '/compute/image';
+ }
+ if (volume) {
+ return '/storage/volume';
+ }
+ if (servergroup) {
+ return `/compute/server-group/detail/${servergroup}`;
+ }
+ return '/compute/instance';
+ }
+ ```
+
+- `steps`
+ - 每一步的配置
+ - 每个配置项
+ - `title`,每一步的标题
+ - `component`,每一步表单对应的组件,继承于`BaseForm`(`src/components/Form`)
+ - 以创建云主机`src/pages/compute/containers/Instance/actions/StepCreate/index.jsx`为例
+ - 包含 4 步:基础配置、网络配置、系统配置、确认配置
+
+ ```javascript
+ get steps() {
+ return [
+ {
+ title: t('Base Config'),
+ component: BaseStep,
+ },
+ {
+ title: t('Network Config'),
+ component: NetworkStep,
+ },
+ {
+ title: t('System Config'),
+ component: SystemStep,
+ },
+ {
+ title: t('Confirm Config'),
+ component: ConfirmStep,
+ },
+ ];
+ }
+ ```
+
+- `onSubmit`
+ - 该操作的请求函数
+ - 操作请求成功后,会自动进入到资源列表页
+ - 操作失败后,会在表单页显示错误提示
+ - 返回`Promise`
+ - 返回表单对应的`store`中的函数
+
+## 按需复写的属性与函数
+
+- `init`
+ - 初始化操作
+ - 在其中定义`this.store`,`loading`状态的展示是基于`this.store.isSubmitting`
+ - 在其中调用获取表单所需其他数据的函数
+ - 对`this.state`中属性的更新
+ - 以创建云主机`src/pages/compute/containers/Instance/actions/StepCreate/index.jsx`为例
+ - 获取配额信息
+
+ ```javascript
+ init() {
+ this.store = globalServerStore;
+ this.projectStore = globalProjectStore;
+ this.getQuota();
+ }
+ ```
+
+- `instanceName`
+ - 请求发送后,提示信息中的资源名称
+ - 默认值为`this.values.name`
+ - 以创建云主机`src/pages/compute/containers/Instance/actions/StepCreate/index.jsx`为例
+ - 如果是批量创建云主机,则按`${name}-${index + 1}`的形式展示名称
+
+ ```javascript
+ get instanceName() {
+ const { name, count = 1 } = this.values || {};
+ if (count === 1) {
+ return this.unescape(name);
+ }
+ return this.unescape(
+ new Array(count)
+ .fill(count)
+ .map((_, index) => `${name}-${index + 1}`)
+ .join(', ')
+ );
+ }
+ ```
+
+- `renderFooterLeft`
+ - 对表单底部左侧内部的渲染
+ - 默认返回`null`
+ - src/pages/compute/containers/Instance/actions/StepCreate/index.jsx
+ - 展示批量创建的数量
+ - 基于输入的数量与剩余配额判定当前表单是否正确
+
+ ```javascript
+ renderFooterLeft() {
+ const { data } = this.state;
+ const { count = 1, source: { value: sourceValue } = {} } = data;
+ const configs = {
+ min: 1,
+ max: sourceValue === 'bootableVolume' ? 1 : 100,
+ precision: 0,
+ onChange: this.onCountChange,
+ formatter: (value) => `$ ${value}`.replace(/\D/g, ''),
+ };
+ return (
+
+ {t('Count')}
+
+ {this.renderBadge()}
+
+ );
+ }
+ ```
+
+- `successText`
+ - 操作成功的提示信息
+- `errorText`
+ - 错误信息的展示
+ - 一般不需要复写
+- `renderFooterLeft`
+ - 表单底部左侧的渲染函数
+
+## 不需要复写的属性与函数
+
+- `values`
+ - 表单验证成功后,更新的表单值
+- `isAdminPage`
+ - 当前页面是否是“管理平台”的页面
+- `getUrl`
+ - 生成页面 Url 的函数
+ - 如:需要给关联资源提供跳转功能,使用该函数,可以在控制台跳转到控制台的相应地址,在管理平台跳转到管理平台的相应地址
+
+## 基类中的基础函数
+
+- `StepAction`继承于`StepForm`
+- 建议查看代码理解,`src/components/StepForm/index.jsx`
diff --git a/docs/zh/develop/images/detail/image-detail-info.png b/docs/zh/develop/images/detail/image-detail-info.png
new file mode 100755
index 00000000..550ed806
Binary files /dev/null and b/docs/zh/develop/images/detail/image-detail-info.png differ
diff --git a/docs/zh/develop/images/detail/volume.png b/docs/zh/develop/images/detail/volume.png
new file mode 100755
index 00000000..e38ddece
Binary files /dev/null and b/docs/zh/develop/images/detail/volume.png differ
diff --git a/docs/zh/develop/images/form/ace-editor.png b/docs/zh/develop/images/form/ace-editor.png
new file mode 100755
index 00000000..56672483
Binary files /dev/null and b/docs/zh/develop/images/form/ace-editor.png differ
diff --git a/docs/zh/develop/images/form/action.png b/docs/zh/develop/images/form/action.png
new file mode 100755
index 00000000..99f25962
Binary files /dev/null and b/docs/zh/develop/images/form/action.png differ
diff --git a/docs/zh/develop/images/form/add-select.png b/docs/zh/develop/images/form/add-select.png
new file mode 100755
index 00000000..5699fb0e
Binary files /dev/null and b/docs/zh/develop/images/form/add-select.png differ
diff --git a/docs/zh/develop/images/form/check-group.png b/docs/zh/develop/images/form/check-group.png
new file mode 100755
index 00000000..fd158f2b
Binary files /dev/null and b/docs/zh/develop/images/form/check-group.png differ
diff --git a/docs/zh/develop/images/form/check.png b/docs/zh/develop/images/form/check.png
new file mode 100755
index 00000000..b4695020
Binary files /dev/null and b/docs/zh/develop/images/form/check.png differ
diff --git a/docs/zh/develop/images/form/confirm.png b/docs/zh/develop/images/form/confirm.png
new file mode 100755
index 00000000..f5070c15
Binary files /dev/null and b/docs/zh/develop/images/form/confirm.png differ
diff --git a/docs/zh/develop/images/form/courgette.log b/docs/zh/develop/images/form/courgette.log
new file mode 100755
index 00000000..e69de29b
diff --git a/docs/zh/develop/images/form/create-success.png b/docs/zh/develop/images/form/create-success.png
new file mode 100755
index 00000000..8c3ea591
Binary files /dev/null and b/docs/zh/develop/images/form/create-success.png differ
diff --git a/docs/zh/develop/images/form/descriptions.png b/docs/zh/develop/images/form/descriptions.png
new file mode 100755
index 00000000..da14393b
Binary files /dev/null and b/docs/zh/develop/images/form/descriptions.png differ
diff --git a/docs/zh/develop/images/form/form-divider.png b/docs/zh/develop/images/form/form-divider.png
new file mode 100755
index 00000000..1cf1aeb6
Binary files /dev/null and b/docs/zh/develop/images/form/form-divider.png differ
diff --git a/docs/zh/develop/images/form/form-extra.png b/docs/zh/develop/images/form/form-extra.png
new file mode 100755
index 00000000..acea6306
Binary files /dev/null and b/docs/zh/develop/images/form/form-extra.png differ
diff --git a/docs/zh/develop/images/form/form-label.png b/docs/zh/develop/images/form/form-label.png
new file mode 100755
index 00000000..0af07be1
Binary files /dev/null and b/docs/zh/develop/images/form/form-label.png differ
diff --git a/docs/zh/develop/images/form/form-tip.png b/docs/zh/develop/images/form/form-tip.png
new file mode 100755
index 00000000..68afc50a
Binary files /dev/null and b/docs/zh/develop/images/form/form-tip.png differ
diff --git a/docs/zh/develop/images/form/input-int.png b/docs/zh/develop/images/form/input-int.png
new file mode 100755
index 00000000..c1431a32
Binary files /dev/null and b/docs/zh/develop/images/form/input-int.png differ
diff --git a/docs/zh/develop/images/form/input-json.png b/docs/zh/develop/images/form/input-json.png
new file mode 100755
index 00000000..50afac5c
Binary files /dev/null and b/docs/zh/develop/images/form/input-json.png differ
diff --git a/docs/zh/develop/images/form/input-name.png b/docs/zh/develop/images/form/input-name.png
new file mode 100755
index 00000000..6d00fb6a
Binary files /dev/null and b/docs/zh/develop/images/form/input-name.png differ
diff --git a/docs/zh/develop/images/form/input-number.png b/docs/zh/develop/images/form/input-number.png
new file mode 100755
index 00000000..bfe9eaf4
Binary files /dev/null and b/docs/zh/develop/images/form/input-number.png differ
diff --git a/docs/zh/develop/images/form/input-password.png b/docs/zh/develop/images/form/input-password.png
new file mode 100755
index 00000000..89dbffb5
Binary files /dev/null and b/docs/zh/develop/images/form/input-password.png differ
diff --git a/docs/zh/develop/images/form/input.png b/docs/zh/develop/images/form/input.png
new file mode 100755
index 00000000..261b4d96
Binary files /dev/null and b/docs/zh/develop/images/form/input.png differ
diff --git a/docs/zh/develop/images/form/instance-action.png b/docs/zh/develop/images/form/instance-action.png
new file mode 100755
index 00000000..b81e693c
Binary files /dev/null and b/docs/zh/develop/images/form/instance-action.png differ
diff --git a/docs/zh/develop/images/form/instance-volume.png b/docs/zh/develop/images/form/instance-volume.png
new file mode 100755
index 00000000..84d9faf1
Binary files /dev/null and b/docs/zh/develop/images/form/instance-volume.png differ
diff --git a/docs/zh/develop/images/form/ip-distributer.png b/docs/zh/develop/images/form/ip-distributer.png
new file mode 100755
index 00000000..df0a23f4
Binary files /dev/null and b/docs/zh/develop/images/form/ip-distributer.png differ
diff --git a/docs/zh/develop/images/form/ip-input.png b/docs/zh/develop/images/form/ip-input.png
new file mode 100755
index 00000000..a5830eee
Binary files /dev/null and b/docs/zh/develop/images/form/ip-input.png differ
diff --git a/docs/zh/develop/images/form/label-col.png b/docs/zh/develop/images/form/label-col.png
new file mode 100755
index 00000000..adcdfb04
Binary files /dev/null and b/docs/zh/develop/images/form/label-col.png differ
diff --git a/docs/zh/develop/images/form/mac-address.png b/docs/zh/develop/images/form/mac-address.png
new file mode 100755
index 00000000..76257eb3
Binary files /dev/null and b/docs/zh/develop/images/form/mac-address.png differ
diff --git a/docs/zh/develop/images/form/member-allocator.png b/docs/zh/develop/images/form/member-allocator.png
new file mode 100755
index 00000000..28c6106d
Binary files /dev/null and b/docs/zh/develop/images/form/member-allocator.png differ
diff --git a/docs/zh/develop/images/form/metadata-transfer.png b/docs/zh/develop/images/form/metadata-transfer.png
new file mode 100755
index 00000000..09befaba
Binary files /dev/null and b/docs/zh/develop/images/form/metadata-transfer.png differ
diff --git a/docs/zh/develop/images/form/modal.png b/docs/zh/develop/images/form/modal.png
new file mode 100755
index 00000000..1b237286
Binary files /dev/null and b/docs/zh/develop/images/form/modal.png differ
diff --git a/docs/zh/develop/images/form/more.png b/docs/zh/develop/images/form/more.png
new file mode 100755
index 00000000..523c926c
Binary files /dev/null and b/docs/zh/develop/images/form/more.png differ
diff --git a/docs/zh/develop/images/form/network-select-table.png b/docs/zh/develop/images/form/network-select-table.png
new file mode 100755
index 00000000..a6fdbda7
Binary files /dev/null and b/docs/zh/develop/images/form/network-select-table.png differ
diff --git a/docs/zh/develop/images/form/network-select.png b/docs/zh/develop/images/form/network-select.png
new file mode 100755
index 00000000..cbfbde74
Binary files /dev/null and b/docs/zh/develop/images/form/network-select.png differ
diff --git a/docs/zh/develop/images/form/page.png b/docs/zh/develop/images/form/page.png
new file mode 100755
index 00000000..c7d7a857
Binary files /dev/null and b/docs/zh/develop/images/form/page.png differ
diff --git a/docs/zh/develop/images/form/port-range.png b/docs/zh/develop/images/form/port-range.png
new file mode 100755
index 00000000..2435f114
Binary files /dev/null and b/docs/zh/develop/images/form/port-range.png differ
diff --git a/docs/zh/develop/images/form/radio.png b/docs/zh/develop/images/form/radio.png
new file mode 100755
index 00000000..4c6eb3e6
Binary files /dev/null and b/docs/zh/develop/images/form/radio.png differ
diff --git a/docs/zh/develop/images/form/select-table-tabs.png b/docs/zh/develop/images/form/select-table-tabs.png
new file mode 100755
index 00000000..0cfc4767
Binary files /dev/null and b/docs/zh/develop/images/form/select-table-tabs.png differ
diff --git a/docs/zh/develop/images/form/select-table.png b/docs/zh/develop/images/form/select-table.png
new file mode 100755
index 00000000..2051f199
Binary files /dev/null and b/docs/zh/develop/images/form/select-table.png differ
diff --git a/docs/zh/develop/images/form/select.png b/docs/zh/develop/images/form/select.png
new file mode 100755
index 00000000..81c595eb
Binary files /dev/null and b/docs/zh/develop/images/form/select.png differ
diff --git a/docs/zh/develop/images/form/slider-input.png b/docs/zh/develop/images/form/slider-input.png
new file mode 100755
index 00000000..815093b5
Binary files /dev/null and b/docs/zh/develop/images/form/slider-input.png differ
diff --git a/docs/zh/develop/images/form/step.png b/docs/zh/develop/images/form/step.png
new file mode 100755
index 00000000..3fe88528
Binary files /dev/null and b/docs/zh/develop/images/form/step.png differ
diff --git a/docs/zh/develop/images/form/switch.png b/docs/zh/develop/images/form/switch.png
new file mode 100755
index 00000000..08c6f6d2
Binary files /dev/null and b/docs/zh/develop/images/form/switch.png differ
diff --git a/docs/zh/develop/images/form/tab-select-table.png b/docs/zh/develop/images/form/tab-select-table.png
new file mode 100755
index 00000000..69011f29
Binary files /dev/null and b/docs/zh/develop/images/form/tab-select-table.png differ
diff --git a/docs/zh/develop/images/form/textarea-from-file.png b/docs/zh/develop/images/form/textarea-from-file.png
new file mode 100755
index 00000000..508422a8
Binary files /dev/null and b/docs/zh/develop/images/form/textarea-from-file.png differ
diff --git a/docs/zh/develop/images/form/textarea.png b/docs/zh/develop/images/form/textarea.png
new file mode 100755
index 00000000..710bd340
Binary files /dev/null and b/docs/zh/develop/images/form/textarea.png differ
diff --git a/docs/zh/develop/images/form/title.png b/docs/zh/develop/images/form/title.png
new file mode 100755
index 00000000..6dcd6d41
Binary files /dev/null and b/docs/zh/develop/images/form/title.png differ
diff --git a/docs/zh/develop/images/form/transfer.png b/docs/zh/develop/images/form/transfer.png
new file mode 100755
index 00000000..dbcc97af
Binary files /dev/null and b/docs/zh/develop/images/form/transfer.png differ
diff --git a/docs/zh/develop/images/form/upload.png b/docs/zh/develop/images/form/upload.png
new file mode 100755
index 00000000..d232c243
Binary files /dev/null and b/docs/zh/develop/images/form/upload.png differ
diff --git a/docs/zh/develop/images/form/volume-action.png b/docs/zh/develop/images/form/volume-action.png
new file mode 100755
index 00000000..8d275f5d
Binary files /dev/null and b/docs/zh/develop/images/form/volume-action.png differ
diff --git a/docs/zh/develop/images/form/volume-select-table.png b/docs/zh/develop/images/form/volume-select-table.png
new file mode 100755
index 00000000..fb51322d
Binary files /dev/null and b/docs/zh/develop/images/form/volume-select-table.png differ
diff --git a/docs/zh/develop/images/form/wrapper-col.png b/docs/zh/develop/images/form/wrapper-col.png
new file mode 100755
index 00000000..1b39ae28
Binary files /dev/null and b/docs/zh/develop/images/form/wrapper-col.png differ
diff --git a/docs/zh/develop/images/i18n/english.png b/docs/zh/develop/images/i18n/english.png
new file mode 100755
index 00000000..7d0bcd73
Binary files /dev/null and b/docs/zh/develop/images/i18n/english.png differ
diff --git a/docs/zh/develop/images/i18n/i18n.png b/docs/zh/develop/images/i18n/i18n.png
new file mode 100755
index 00000000..94e40cf2
Binary files /dev/null and b/docs/zh/develop/images/i18n/i18n.png differ
diff --git a/docs/zh/develop/images/list/batch.png b/docs/zh/develop/images/list/batch.png
new file mode 100755
index 00000000..8909e73a
Binary files /dev/null and b/docs/zh/develop/images/list/batch.png differ
diff --git a/docs/zh/develop/images/list/download.png b/docs/zh/develop/images/list/download.png
new file mode 100755
index 00000000..14b10ffc
Binary files /dev/null and b/docs/zh/develop/images/list/download.png differ
diff --git a/docs/zh/develop/images/list/fresh.png b/docs/zh/develop/images/list/fresh.png
new file mode 100755
index 00000000..5847c0bb
Binary files /dev/null and b/docs/zh/develop/images/list/fresh.png differ
diff --git a/docs/zh/develop/images/list/hide.png b/docs/zh/develop/images/list/hide.png
new file mode 100755
index 00000000..57279eb7
Binary files /dev/null and b/docs/zh/develop/images/list/hide.png differ
diff --git a/docs/zh/develop/images/list/pagination.png b/docs/zh/develop/images/list/pagination.png
new file mode 100755
index 00000000..37f79cc8
Binary files /dev/null and b/docs/zh/develop/images/list/pagination.png differ
diff --git a/docs/zh/develop/images/list/search-example.png b/docs/zh/develop/images/list/search-example.png
new file mode 100755
index 00000000..798b6494
Binary files /dev/null and b/docs/zh/develop/images/list/search-example.png differ
diff --git a/docs/zh/develop/images/list/search.png b/docs/zh/develop/images/list/search.png
new file mode 100755
index 00000000..72a6ec00
Binary files /dev/null and b/docs/zh/develop/images/list/search.png differ
diff --git a/docs/zh/develop/images/list/stop-auto-refresh.png b/docs/zh/develop/images/list/stop-auto-refresh.png
new file mode 100755
index 00000000..9ce9e915
Binary files /dev/null and b/docs/zh/develop/images/list/stop-auto-refresh.png differ
diff --git a/docs/zh/develop/images/list/tab-list.png b/docs/zh/develop/images/list/tab-list.png
new file mode 100755
index 00000000..5f8d6829
Binary files /dev/null and b/docs/zh/develop/images/list/tab-list.png differ
diff --git a/docs/zh/develop/images/list/tab-service.png b/docs/zh/develop/images/list/tab-service.png
new file mode 100755
index 00000000..97397d0a
Binary files /dev/null and b/docs/zh/develop/images/list/tab-service.png differ
diff --git a/docs/zh/develop/images/list/volumes.png b/docs/zh/develop/images/list/volumes.png
new file mode 100755
index 00000000..8d6c3337
Binary files /dev/null and b/docs/zh/develop/images/list/volumes.png differ
diff --git a/docs/zh/develop/images/menu/admin-menu.png b/docs/zh/develop/images/menu/admin-menu.png
new file mode 100755
index 00000000..04f5b516
Binary files /dev/null and b/docs/zh/develop/images/menu/admin-menu.png differ
diff --git a/docs/zh/develop/images/menu/console-menu.png b/docs/zh/develop/images/menu/console-menu.png
new file mode 100755
index 00000000..7ae421ac
Binary files /dev/null and b/docs/zh/develop/images/menu/console-menu.png differ
diff --git a/docs/zh/develop/images/store/response-key.png b/docs/zh/develop/images/store/response-key.png
new file mode 100755
index 00000000..2563986e
Binary files /dev/null and b/docs/zh/develop/images/store/response-key.png differ
diff --git a/docs/zh/test/1-ready-to-work.md b/docs/zh/test/1-ready-to-work.md
new file mode 100644
index 00000000..c97207ff
--- /dev/null
+++ b/docs/zh/test/1-ready-to-work.md
@@ -0,0 +1,142 @@
+简体中文 | [English](/docs/en/test/1-ready-to-work.md)
+
+# 两种测试
+
+我们提供了两种类型的测试
+
+- E2E 测试
+ - 侧重于功能点测试
+ - 能提供代码覆盖率数据
+ - 使用`Cypress`框架
+ - 测试结果保存到便于预览的静态页面中
+- 单元测试
+ - 侧重于基础函数测试
+ - 使用`Jest`框架
+
+# E2E 测试
+
+## 搭建 E2E 测试环境
+
+在 Centos,Windows 的 wsl2 中均成功搭建过 E2E 测试环境
+
+- node 环境
+ - package.json 中要求:`"node": ">=10.22.0"`
+ - 验证 nodejs 版本
+
+ ```shell
+ node -v
+ ```
+
+- yarn
+ - 安装 yarn
+
+ ```shell
+ npm install -g yarn
+ ```
+
+- 安装依赖包
+ - 在项目根目录下执行,即`package.json`同级,需要耐心等待安装完成
+
+ ```shell
+ yarn install
+ ```
+
+- 安装系统依赖
+ - `Ubuntu/Debian`
+
+ ```shell
+ sudo apt-get install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb
+ ```
+
+ - `CentOS`
+
+ ```shell
+ yum install -y xorg-x11-server-Xvfb gtk2-devel gtk3-devel libnotify-devel GConf2 nss libXScrnSaver alsa-lib
+ ```
+
+- 调整访问路径、账号等信息
+ - E2E 的配置文件存放于`test/e2e/config/config.yaml`,在其中配置了
+ - `baseUrl`,测试访问路径
+ - `env`,环境变量
+ - `switchToAdminProject`,登录后是否需要切换到`admin`项目下
+ - `username`,访问控制台的用户名,需要具有控制台操作权限的用户
+ - `password`,访问控制台的密码
+ - `usernameAdmin`,访问管理平台的用户名,需要具有管理平台操作权限的用户
+ - `passwordAdmin`,访问管理平台的密码
+ - `testFiles`,测试文件列表
+ - 可以通过直接修改`config.yaml`中的相应数值完成配置变更
+ - 也可以通过`local_config.yaml`完成配置变更
+ - 复制`test/e2e/config/config.yaml`到`test/e2e/config/local_config.yaml`中
+ - 修改`local_config.yaml`中的相应变量
+ - 对于变量的取值,优先级为:`local_config.yaml` > `config.yaml`
+
+## 命令行运行 E2E
+
+```shell
+yarn run test:e2e
+```
+
+![控制台](/docs/zh/test/images/e2e/console.png)
+
+## GUI 运行 E2E
+
+```shell
+yarn run test:e2e:open
+```
+
+Cypress 提供了 GUI
+
+![gui](/docs/zh/test/images/e2e/gui-list.png)
+
+![work](/docs/zh/test/images/e2e/gui-work.png)
+
+## E2E 测试结果
+
+测试运行结束后,访问`test/e2e/report/merge-report.html`即可查看
+
+![结果](/docs/zh/test/images/e2e/result.png)
+
+## E2E 代码覆盖率测试结果
+
+测试运行结束后,访问`coverage/lcov-report/index.html`即可查看
+
+> 注意:代码覆盖率,需要 E2E 访问的`baseUrl`对应的前端包,是具有可检测代码覆盖率版本的`dist`包
+
+```shell
+yarn run build:test
+```
+
+以上述方式打包的文件,就是具有可测试代码覆盖率的前端包
+
+以下,给出前端访问带有代码覆盖率功能的前端包的 nginx 配置
+
+```nginx
+server {
+ listen 0.0.0.0:8088 default_server;
+
+ root /path/to/skyline-console/dist;
+ index index.html;
+ server_name _;
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+
+ location /api {
+ proxy_pass http://;
+ }
+}
+```
+
+# 单元测试
+
+## 命令行运行单元测试
+
+```shell
+yarn run test:unit
+```
+
+## 单元测试结果
+
+直接在命令行控制台中即可查看运行结果
+
+![单元测试结果](/docs/zh/test/images/unit/result.png)
diff --git a/docs/zh/test/2-catalog-introduction.md b/docs/zh/test/2-catalog-introduction.md
new file mode 100644
index 00000000..f14c6c24
--- /dev/null
+++ b/docs/zh/test/2-catalog-introduction.md
@@ -0,0 +1,91 @@
+简体中文 | [English](/docs/en/test/2-catalog-introduction.md)
+
+```
+test
+├── e2e (E2E代码存放位置)
+│ ├── config
+│ │ ├── config.yaml (E2E运行时的部分配置,主要配置了测试用例文件列表,登录账号等信息)
+│ │ └── local_config.yaml (E2E运行时的部分配置,主要配置了测试用例文件列表,登录账号等信息,是gitignore的,优先级高于config.yaml)
+│ ├── fixtures (存放运行时需要的上传文件,读取文件等)
+│ │ ├── keypair (测试密钥读取的文件)
+│ │ ├── metadata.json (测试元数据读取的文件)
+│ │ ├── stack-content.yaml (测试堆栈读取的文件)
+│ │ └── stack-params.yaml (测试堆栈读取的文件)
+│ ├── integration (存放测试用例)
+│ │ └── pages (按网页菜单结构调整目录)
+│ │ ├── compute (计算)
+│ │ │ ├── aggregate.spec.js (主机集合)
+│ │ │ ├── baremetal.spec.js (裸机配置)
+│ │ │ ├── flavor.spec.js (云主机类型)
+│ │ │ ├── hypervisor.spec.js (虚拟机管理器)
+│ │ │ ├── image.spec.js (镜像)
+│ │ │ ├── instance.spec.js (云主机)
+│ │ │ ├── ironic.spec.js (裸机)
+│ │ │ ├── keypair.spec.js (密钥)
+│ │ │ └── server-group.spec.js (云主机组)
+│ │ ├── configuration (平台配置)
+│ │ │ ├── metadata.spec.js (元数据)
+│ │ │ └── system.spec.js (系统信息)
+│ │ ├── error.spec.js (错误页面)
+│ │ ├── heat (资源编排)
+│ │ │ └── stack.spec.js (堆栈)
+│ │ ├── identity (身份管理)
+│ │ │ ├── domain.spec.js (域)
+│ │ │ ├── project.spec.js (项目)
+│ │ │ ├── role.spec.js (角色)
+│ │ │ ├── user-group.spec.js (用户组)
+│ │ │ └── user.spec.js (用户)
+│ │ ├── login.spec.js (登录)
+│ │ ├── management (运维管理)
+│ │ │ └── recycle-bin.spec.js (回收站)
+│ │ ├── network (网络)
+│ │ │ ├── floatingip.spec.js (浮动IP)
+│ │ │ ├── lb.spec.js (负载均衡)
+│ │ │ ├── network.spec.js (网络)
+│ │ │ ├── qos-policy.spec.js (Qos策略)
+│ │ │ ├── router.spec.js (路由器)
+│ │ │ ├── security-group.spec.js (安全组)
+│ │ │ ├── topology.spec.js (网络拓扑)
+│ │ │ ├── virtual-adapter.spec.js (虚拟网卡)
+│ │ │ └── vpn.spec.js (VPN)
+│ │ └── storage (存储)
+│ │ ├── backup.spec.js (备份)
+│ │ ├── qos.spec.js (QoS)
+│ │ ├── snapshot.spec.js (云硬盘快照)
+│ │ ├── storage.spec.js (存储后端)
+│ │ ├── volume-type.spec.js (云硬盘类型)
+│ │ └── volume.spec.js (云硬盘)
+│ ├── plugins (Cypress的扩展)
+│ │ └── index.js (配置了对配置文件的读取,配置了使用代码覆盖率功能)
+│ ├── report (存放E2E的测试报告)
+│ │ ├── merge-report.html (最终生成的测试报告,记录了每个用例的执行情况)
+│ │ └── merge-report.json (results目录下的测试结果的汇总)
+│ ├── results (存放测试用的结果文件)
+│ ├── screenshots (存放测试出错时的快照)
+│ ├── support (编写测试用例时,二次封装的函数)
+│ │ ├── commands.js (存放登录、登出等操作函数)
+│ │ ├── common.js (存放基础函数)
+│ │ ├── constants.js (存放每个资源的路由)
+│ │ ├── detail-commands.js (存放资源详情页相关的函数,基于框架,详情页的操作具有一致性)
+│ │ ├── form-commands.js (存放表单相关的函数,基于框架,对表单项的操作具有一致性)
+│ │ ├── index.js
+│ │ ├── resource-commands.js (存放资源操作相关的函数,如:创建云主机、创建路由、删除资源等)
+│ │ └── table-commands.js (存放资源列表相关的函数,基于框架,对列表的操作具有一致性)
+│ └── utils (存放对于配置文件的读取函数)
+│ └── index.js
+└── unit (单元测试)
+ ├── local-storage-mock.js (本地存储的mock函数)
+ ├── locales (测试国际化时使用的翻译文件)
+ │ ├── en-US.js
+ │ └── zh-CN.js
+ ├── setup-tests.js (配置单元测试)
+ └── svg-mock.js (图片加载的mock)
+```
+
+- E2E 测试的代码,存放在`test/e2e`目录下
+ - E2E 的其他全局配置,存放在`cypress.json`
+- 单元测试的基础代码,存放在`test/unit`目录下
+ - 单元测试的其他全局配置,存放在`jest.config.js`
+ - 单元测试的测试代码,通常是与待测试文件放在相同的目录下,并以`test.js`或`spec.js`为后缀
+ - 如:`src/utils/index.js`与`src/utils/index.test.js`
+ - 如:`src/utils/local-storage.js`与`src/utils/local-storage.spec.js`
diff --git a/docs/zh/test/3-0-how-to-edit-e2e-case.md b/docs/zh/test/3-0-how-to-edit-e2e-case.md
new file mode 100644
index 00000000..98c181f9
--- /dev/null
+++ b/docs/zh/test/3-0-how-to-edit-e2e-case.md
@@ -0,0 +1,104 @@
+简体中文 | [English](/docs/en/test/3-0-how-to-edit-e2e-case.md)
+
+关于 Cypress 的具体介绍及使用方法,请参考[官方文档](https://docs.cypress.io/guides/overview/why-cypress)
+
+这里主要给出编写 Skyline-console 前端页面中,资源对应的 E2E 用例,并使用`test/e2e/support`中定义的函数的说明
+
+以下介绍,以云主机用例`test/e2e/integration/pages/compute/instance.spec.js`为例
+
+一般,测试资源的相应功能时,是按照以下顺序
+
+1. 准备测试使用的相关变量
+ - 创建资源时的必须参数,如:名称、密码
+ - 编辑资源时的必须参数,如:新的名称
+ - 创建关联资源时,关联资源的名称,如:网络名称、路由器名称、云硬盘名称
+
+ ```javascript
+ const uuid = Cypress._.random(0, 1e6);
+ const name = `e2e-instance-${uuid}`;
+ const newname = `${name}-1`;
+ const password = 'passw0rd_1';
+ const volumeName = `e2e-instance-attach-volume-${uuid}`;
+ const networkName = `e2e-network-for-instance-${uuid}`;
+ const routerName = `e2e-router-for-instance-${uuid}`;
+ ```
+
+2. 操作前登录
+ - 如果是操作控制台资源,使用`cy.login`
+ - 如果是操作管理平台资源,使用`cy.loginAdmin`
+ - 一般会在`login`与`loginAdmin`函数中使用变量`listUrl`,即登录后直接访问资源所在页面
+
+ ```javascript
+ beforeEach(() => {
+ cy.login(listUrl);
+ });
+ ```
+
+3. 创建关联资源,使用`resource-commands.js`中提供的创建资源的函数,以测试云主机为例
+ - 创建网络,用于测试创建云主机、挂载网卡
+
+ ```javascript
+ cy.createNetwork({ name: networkName });
+ ```
+
+ - 创建路由器`cy.createRouter`,用于测试关联浮动 IP 时确保浮动 IP 可达
+ - 以如下方式创建的路由器将开启外网网关,并绑定了`networkName`网络的子网
+
+ ```javascript
+ cy.createRouter({ name: routerName, network: networkName });
+ ```
+
+ - 创建浮动 IP`cy.createFip`,用于测试关联浮动 IP
+
+ ```javascript
+ cy.createFip();
+ ```
+
+ - 创建云硬盘`cy.createVolume`(用于测试挂载云硬盘)
+
+ ```javascript
+ cy.createVolume(volumeName);
+ ```
+
+4. 编写创建资源的用例
+5. 编写访问资源详情的用例
+6. 分别编写资源的所有操作对应的用例
+ - 一般`编辑`操作的用例写在后面,其后编写`删除`操作的用例,这样能测试到编辑是否生效
+7. 删除关联资源,使用`resource-commands.js`中提供的删除资源的函数,这是为了测试用例执行后,测试账号内的资源尽可能的干净
+ - 删除浮动 IP
+
+ ```javascript
+ cy.deleteAll('fip');
+ ```
+
+ - 删除路由器`routerName`
+
+ ```javascript
+ cy.deleteRouter(routerName, networkName);
+ ```
+
+ - 删除网络`networkName`
+
+ ```javascript
+ cy.deleteAll('network', networkName);
+ ```
+
+ - 删除云硬盘`volumeName`
+
+ ```javascript
+ cy.deleteAll('volume', volumeName);
+ ```
+
+ - 删除所有可用状态的云硬盘
+
+ ```javascript
+ cy.deleteAllAvailableVolume();
+ ```
+
+上述步骤中的`4`、`5`、`6`主要使用了
+
+- `test/e2e/support/form-commands.js`中的函数操作表单,详细介绍见[3-1-E2E-form-operation](3-1-E2E-form-operation.md)
+- `test/e2e/support/table-commands.js`中的函数,操作表格中的按钮点击、搜索、进入详情,详细介绍见[3-2-E2E-table-operation](3-2-E2E-table-operation.md)
+- `test/e2e/support/detail-commands.js`中的函数,操作返回列表页、检测详情内容、切换详情 Tab,详细介绍见[3-3-E2E-detail-operation](3-3-E2E-detail-operation.md)
+
+创建、删除关联资源主要使用了`test/e2e/support/resource-commands.js`中的函数,,详细介绍见[3-4-E2E-resource-operation](3-4-E2E-resource-operation.md)
\ No newline at end of file
diff --git a/docs/zh/test/3-1-E2E-form-operation.md b/docs/zh/test/3-1-E2E-form-operation.md
new file mode 100644
index 00000000..d5a7076c
--- /dev/null
+++ b/docs/zh/test/3-1-E2E-form-operation.md
@@ -0,0 +1,591 @@
+简体中文 | [English](/docs/en/test/3-1-E2E-form-operation.md)
+
+因为前端框架使用的一致性,我们在编写表单操作的相关用例,选取元素并进行操作时,往往会发现有很强的规律性,所以我们对大多数表单操作都编写了相应的 Cypress 函数,极大的减少了编写测试用例的难度,以下会对主要使用的表单操作函数做出详细的说明。
+
+> 注意:编写的函数均以能完整完成对一个表单项的操作为原则
+
+## 点击按钮的操作
+
+- `closeNotice`
+ - 关闭操作后右上角的操作成功的提示信息
+
+ ![notice](/docs/zh/test/images/e2e/form/notice.png)
+
+- `waitFormLoading`
+ - 等待表单请求完成
+ - 表单填写并验证通过后,点击确认按钮,会向服务端发起相应请求,这时表单项的确认按钮会处于`Loading`的状态
+ - 使用该函数,而不是`cy.wait(seconds)`,能更有效的保证同步请求已经处理完全,从而保证后续用例的先决条件
+
+ ![wait-form-loading](/docs/zh/test/images/e2e/form/wait-form-loading.png)
+
+- `clickFormActionSubmitButton`
+ - 点击确认型表单的确认按钮,并等待请求完成
+
+ ![click-form-submit](/docs/zh/test/images/e2e/form/click-form-submit.png)
+
+- `clickModalActionSubmitButton`
+ - 点击弹窗型表单的确认按钮,并等待请求完成
+
+ ![click-modal-submit](/docs/zh/test/images/e2e/form/click-modal-submit.png)
+
+- `clickModalActionCancelButton`
+ - 点击弹窗型表单的取消按钮
+- `clickConfirmActionSubmitButton`
+ - 点击确认型表单的确认按钮,等待请求完成,并关闭请求成功的提示信息
+ - 参数`waitTime`,关闭提示信息后的等待时间
+
+ ![click-confirm-submit](/docs/zh/test/images/e2e/form/click-confirm-submit.png)
+
+- `checkDisableAction`
+ - 某些数据不符合要求时,使用批量操作,会弹出报错,该函数验证该数据的确不合操作要求,并关闭报错提示
+ - 以锁定状态的云主机`test/e2e/integration/pages/compute/instance.spec.js`为例
+ - 锁定后不再支持启动、关闭、重启操作
+
+ ```javascript
+ it('successfully lock', () => {
+ cy.tableSearchText(name)
+ .clickConfirmActionInMoreSub('Lock', 'Instance Status')
+ .wait(10000);
+ cy.tableSearchText(name)
+ .selectFirst()
+ .clickHeaderButtonByTitle('Start')
+ .checkDisableAction(2000)
+ .clickHeaderButtonByTitle('Stop')
+ .checkDisableAction(2000)
+ .clickHeaderButtonByTitle('Reboot')
+ .checkDisableAction(2000);
+ });
+ ```
+
+ ![disable-action](/docs/zh/test/images/e2e/form/disable-action.png)
+
+- `clickStepActionNextButton`
+ - 点击分步表单的下一步/确认按钮
+ - 以创建云主机用例`test/e2e/integration/pages/compute/instance.spec.js`为例
+ - 共需要点击 3 次下一步,1 次确认按钮
+
+ ```javascript
+ it('successfully create', () => {
+ cy.clickHeaderButton(1)
+ .url()
+ .should('include', `${listUrl}/create`)
+ .wait(5000)
+ .formTableSelect('flavor')
+ .formTableSelect('image')
+ .formSelect('systemDisk')
+ .formAddSelectAdd('dataDisk')
+ .formSelect('dataDisk')
+ .wait(2000)
+ .clickStepActionNextButton()
+ .wait(5000)
+ .formTableSelectBySearch('networkSelect', networkName, 5000)
+ .formTableSelectBySearch('securityGroup', 'default', 5000)
+ .wait(2000)
+ .clickStepActionNextButton()
+ .formInput('name', name)
+ .formRadioChoose('loginType', 1)
+ .formInput('password', password)
+ .formInput('confirmPassword', password)
+ .wait(2000)
+ .clickStepActionNextButton()
+ .wait(2000)
+ .clickStepActionNextButton()
+ .waitFormLoading()
+ .url()
+ .should('include', listUrl)
+ .closeNotice()
+ .waitStatusActiveByRefresh();
+ });
+ ```
+
+ ![click-step-next](/docs/zh/test/images/e2e/form/click-step-next.png)
+
+- `clickStepActionCancelButton`
+ - 点击分步表单的取消按钮
+ - 以镜像创建云主机用例`test/e2e/integration/pages/compute/image.spec.js`为例
+ - 只验证能成功进入到创建云主机页面,然后点击取消按钮完成该用例
+
+ ```javascript
+ it('successfully create instance with cancel', () => {
+ cy.tableSearchText(name)
+ .clickActionInMore('Create Instance')
+ .wait(2000)
+ .clickStepActionCancelButton();
+ });
+ ```
+
+## 对表单项的操作
+
+通过页面查看元素的结构、样式等,发现,所有的表单项,都具有`id`,而且对应于开发时编写的表单配置`formItem`的`name`属性,也可直接通过查看页面内元素的`id`获取`name`,如下图所示,`form-item-col-`之后的内容便是`name`
+
+![form-name](/docs/zh/test/images/e2e/form/form-name.png)
+
+- `formInput`
+ - 带有`input`输入框的表单项输入内容
+ - 参数`formItemName`,即开发代码中`formItem`的`name`值
+ - 参数`value`,输入的内容
+ - 以编辑云主机用例`test/e2e/integration/pages/compute/instance.spec.js`为例
+
+ ```javascript
+ it('successfully edit', () => {
+ cy.tableSearchText(name)
+ .clickActionInMore('Edit')
+ .formInput('name', newname)
+ .clickModalActionSubmitButton()
+ .wait(2000);
+ });
+ ```
+
+ ![input](/docs/zh/test/images/e2e/form/input.png)
+
+- `formJsonInput`
+ - 带有`textarea`输入框的表单项输入`json`格式内容
+ - 参数`formItemName`,即开发代码中`formItem`的`name`值
+ - 参数`content`,输入的对象
+ - 以创建堆栈,编写`json`型的参数`test/e2e/integration/pages/heat/stack.spec.js`为例
+
+ ```javascript
+ it('successfully create', () => {
+ const volumeJson = {
+ name: volumeName,
+ };
+ cy.clickHeaderButton(1, 2000)
+ .formAttachFile('content', contentFile)
+ .formAttachFile('params', paramFile)
+ .clickStepActionNextButton()
+ .wait(2000)
+ .formInput('name', name)
+ .formJsonInput('volume_name_spec', volumeJson)
+ .clickStepActionNextButton()
+ .waitFormLoading()
+ .wait(5000)
+ .tableSearchSelectText('Name', name)
+ .waitStatusActiveByRefresh();
+ });
+ ```
+
+ ![textarea-json](/docs/zh/test/images/e2e/form/textarea-json.png)
+
+- `formCheckboxClick`
+ - 点击表单项中的`checkbox`
+ - 参数`formItemName`,即开发代码中`formItem`的`name`值
+ - 参数`index`,默认为`0`
+ - 以云主机修改配置`test/e2e/integration/pages/compute/instance.spec.js`为例
+
+ ```javascript
+ it('successfully resize', () => {
+ cy.tableSearchText(name)
+ .clickActionInMoreSub('Resize', 'Configuration Update')
+ .wait(5000)
+ .formTableSelect('newFlavor')
+ .formCheckboxClick('option')
+ .clickModalActionSubmitButton()
+ .waitStatusActiveByRefresh();
+ });
+ ```
+
+ ![checkbox](/docs/zh/test/images/e2e/form/checkbox.png)
+
+- `formTableSelectAll`
+ - 对表格选择类型的表单项做全选操作
+ - 参数`formItemName`,即开发代码中`formItem`的`name`值
+ - 以云硬盘类型修改访问`test/e2e/integration/pages/storage/volume-type.spec.js`为例
+
+ ```javascript
+ it('successfully manage access to projects', () => {
+ cy.tableSearchText(name)
+ .clickActionInMore('Manage Access')
+ .formCheckboxClick('isPublic')
+ .formTableSelectAll('access')
+ .clickModalActionSubmitButton();
+ });
+ ```
+
+ ![select-all](/docs/zh/test/images/e2e/form/select-all.png)
+
+- `formTableNotSelectAll`
+ - 对表格选择类型的表单项做取消全选操作
+ - 参数`formItemName`,即开发代码中`formItem`的`name`值
+ - 以主机集合管理主机时不选择主机`test/e2e/integration/pages/compute/aggregate.spec.js`为例
+
+ ```javascript
+ it('successfully manage host: no host', () => {
+ cy.tableSearchText(newname)
+ .clickActionInMore('Manage Host')
+ .formTableNotSelectAll('hosts')
+ .clickModalActionSubmitButton();
+ });
+ ```
+
+ ![unselect-all](/docs/zh/test/images/e2e/form/unselect-all.png)
+
+- `formTableSelect`
+ - 对表格选择类型的表单项做选择操作
+ - 参数`formItemName`,即开发代码中`formItem`的`name`值
+ - 参数`value`,如果设置`value`,则选择表格中含有该值的条目,如果不设置`value`,则选择表格中的第一个条目
+ - 以云主机挂载网卡选择网络`test/e2e/integration/pages/compute/instance.spec.js`为例
+
+ ```javascript
+ it('successfully attach interface', () => {
+ cy.tableSearchText(name)
+ .clickActionInMoreSub('Attach Interface', 'Related Resources')
+ .wait(5000)
+ .formTableSelect('network')
+ .clickModalActionSubmitButton();
+ });
+ ```
+
+ ![select-table](/docs/zh/test/images/e2e/form/select-table.png)
+
+- `formTableSelectBySearch`
+ - 对表格选择类型的表单项,先做搜索操作,然后选择条目中的第一条
+ - 参数`formItemName`,即开发代码中`formItem`的`name`值
+ - 参数`value`,搜索内容,一般是对搜索项中`名称`的搜索
+ - 参数`waitTime`,搜索后等待时间,不设置,等待 2 秒钟
+ - 以云主机挂载云硬盘选择云硬盘`test/e2e/integration/pages/compute/instance.spec.js`为例
+
+ - 操作成功后,进入云硬盘列表页查看云硬盘的状态为“已使用”
+
+ ```javascript
+ it('successfully attach volume', () => {
+ // prepair volume
+ cy.visitPage(listUrl)
+ .tableSearchText(name)
+ .clickActionInMoreSub('Attach Volume', 'Related Resources')
+ .wait(5000)
+ .formTableSelectBySearch('volume', volumeName)
+ .clickModalActionSubmitButton()
+ .wait(5000);
+
+ // check attach successful
+ cy.tableSearchText(name)
+ .goToDetail()
+ .clickDetailTab('Volume')
+ .tableSearchText(volumeName)
+ .checkColumnValue(2, 'In-use');
+ });
+ ```
+
+ ![select-table-search](/docs/zh/test/images/e2e/form/select-table-search.png)
+
+- `formTableSelectBySearchOption`
+ - 对表格选择类型的表单项,先做搜索操作,然后选择条目中的第一条
+ - 搜索是对搜索项的选择,不同于`formTableSelectBySearch`是基于输入
+ - 参数`formItemName`,即开发代码中`formItem`的`name`值
+ - 参数`name`,搜索项的名称
+ - 参数`value`,搜索项对应的值
+ - 参数`waitTime`,搜索后的等待时间,默认为 2 秒
+ - 以创建全量备份`test/e2e/integration/pages/storage/backup.spec.js`为例
+ - 选择状态为使用中的云硬盘
+
+ ```javascript
+ it('successfully create full bakcup', () => {
+ cy.clickHeaderButton(1, 5000)
+ .formInput('name', name)
+ .formTableSelectBySearch('volume', volumeName)
+ .clickModalActionSubmitButton()
+ .wait(5000)
+ .waitTableLoading();
+
+ cy.clickHeaderButton(1, 5000)
+ .formInput('name', nameIns)
+ .formTableSelectBySearchOption('volume', 'Status', 'In-use')
+ .clickModalActionSubmitButton();
+
+ cy.wait(30000);
+ });
+ ```
+
+ ![select-table-option](/docs/zh/test/images/e2e/form/select-table-option.png)
+
+- `formSelect`
+ - 对选择器类型的表单项的操作
+ - 参数`formItemName`,即开发代码中`formItem`的`name`值
+ - 参数`label`,选中的内容,如果不设置,选中第一个选项,如果设置,选择`label`对应的选项
+ - 以创建云主机组选择策略`test/e2e/integration/pages/compute/server-group.spec.js`为例
+
+ ```javascript
+ it('successfully create', () => {
+ cy.clickHeaderButton(1)
+ .formInput('name', name)
+ .formSelect('policy')
+ .clickModalActionSubmitButton();
+ });
+ ```
+
+ ![select](/docs/zh/test/images/e2e/form/select.png)
+
+ - 以网络 QoS 策略创建带宽限制规则时设置方向为“入方向”`test/e2e/integration/pages/network/qos-policy.spec.js`为例
+
+ ```javascript
+ it('successfully create bandwidth ingress limit rule', () => {
+ cy.tableSearchText(name)
+ .clickActionInMore('Create Bandwidth Limit Rule')
+ .formSelect('direction', 'ingress')
+ .clickModalActionSubmitButton();
+ });
+ ```
+
+ ![select-value](/docs/zh/test/images/e2e/form/select-value.png)
+
+- `formRadioChoose`
+ - 对单选类型的表单项的操作
+ - 参数`formItemName`,即开发代码中`formItem`的`name`值
+ - 参数`itemIndex`,选中第几项,默认为 0,即选择第一项
+ - 以创建密钥选择“导入密钥”`test/e2e/integration/pages/compute/keypair.spec.js`为例
+
+ ```javascript
+ it('successfully create by file', () => {
+ cy.clickHeaderButton(1)
+ .formRadioChoose('type', 1)
+ .formInput('name', nameByFile)
+ .formAttachFile('public_key', filename)
+ .clickModalActionSubmitButton()
+ .tableSearchText(nameByFile)
+ .checkTableFirstRow(nameByFile);
+ });
+ ```
+
+ ![radio](/docs/zh/test/images/e2e/form/radio.png)
+
+- `formAttachFile`
+ - 对上传文件的表单项的操作
+ - 参数`formItemName`,即开发代码中`formItem`的`name`值
+ - 参数`filename`,上传文件的名称,文件需要预先保存在`test/e2e/fixtures`目录下
+ - 以创建密钥选择文件为例`test/e2e/integration/pages/compute/keypair.spec.js`为例
+
+ ```javascript
+ it('successfully create by file', () => {
+ cy.clickHeaderButton(1)
+ .formRadioChoose('type', 1)
+ .formInput('name', nameByFile)
+ .formAttachFile('public_key', filename)
+ .clickModalActionSubmitButton()
+ .tableSearchText(nameByFile)
+ .checkTableFirstRow(nameByFile);
+ });
+ ```
+
+ ![attach-file](/docs/zh/test/images/e2e/form/attach-file.png)
+
+ - 以创建镜像选择文件为例`test/e2e/integration/pages/compute/image.spec.js`为例
+
+ ```javascript
+ it('successfully create', () => {
+ cy.clickHeaderButton(1)
+ .url()
+ .should('include', `${listUrl}/create`)
+ .formInput('name', name)
+ .formAttachFile('file', filename)
+ .formSelect('disk_format', 'QCOW2 - QEMU Emulator')
+ .formSelect('os_distro', 'Others')
+ .formInput('os_version', 'cirros-0.4.0-x86_64')
+ .formInput('os_admin_user', 'root')
+ .formSelect('usage_type', 'Common Server')
+ .formText('description', name)
+ .clickFormActionSubmitButton()
+ .wait(2000)
+ .url()
+ .should('include', listUrl)
+ .tableSearchText(name)
+ .waitStatusActiveByRefresh();
+ });
+ ```
+
+ ![attach-file-image](/docs/zh/test/images/e2e/form/attach-file-image.png)
+
+- `formAddSelectAdd`
+ - 对可增加条目的表单项的操作
+ - 参数`formItemName`,即开发代码中`formItem`的`name`值
+ - 以主机集合管理元数据时,添加新的自定义元数据`test/e2e/integration/pages/compute/aggregate.spec.js`为例
+
+ ```javascript
+ it('successfully manage metadata', () => {
+ cy.tableSearchText(name)
+ .clickActionInMore('Manage Metadata')
+ .wait(5000)
+ .formAddSelectAdd('customs')
+ .formInputKeyValue('customs', 'key', 'value')
+ .formTransferLeftCheck('systems', 0)
+ .clickModalActionSubmitButton();
+ });
+ ```
+
+ ![add-select](/docs/zh/test/images/e2e/form/add-select.png)
+
+- `formSwitch`
+ - 对开关型的表单项的点击操作
+ - 参数`formItemName`,即开发代码中`formItem`的`name`值
+ - 以创建具有共享属性的网络 QoS 策略`test/e2e/integration/pages/network/qos-policy.spec.js`为例
+
+ ```javascript
+ it('successfully create', () => {
+ cy.clickHeaderButton(1)
+ .wait(2000)
+ .formInput('name', name)
+ .formText('description', name)
+ .formSwitch('shared')
+ .clickModalActionSubmitButton();
+ });
+ ```
+
+ ![switch](/docs/zh/test/images/e2e/form/switch.png)
+
+- `formButtonClick`
+ - 对表单项中的按钮的点击操作
+ - 参数`formItemName`,即开发代码中`formItem`的`name`值
+ - 以项目更改配额时展开/关闭“高级选项”`test/e2e/integration/pages/identity/project.spec.js`为例
+
+ ```javascript
+ it('successfully edit quota', () => {
+ cy.tableSearchText(name)
+ .clickActionInMore('Edit Quota')
+ .formInput('instances', 11)
+ .formButtonClick('more')
+ .wait(2000)
+ .formButtonClick('more')
+ .clickModalActionSubmitButton();
+ });
+ ```
+
+ ![more](/docs/zh/test/images/e2e/form/more.png)
+
+ ![more-open](/docs/zh/test/images/e2e/form/more-open.png)
+
+- `formTransfer`
+ - 对穿梭框类型的表单操作
+ 1. 对左侧的穿梭框基于搜索展示指定待选条目
+ 2. 选中待选条目的第一条
+ 3. 点击穿梭框中间的方向按钮,使得选中内容进入到右侧穿梭框中
+ - 参数`formItemName`,即开发代码中`formItem`的`name`值
+ - 参数`value`,搜索内容
+ - 以项目管理用户`test/e2e/integration/pages/identity/project.spec.js`为例
+
+ ```javascript
+ it('successfully manage user', () => {
+ cy.tableSearchText(name)
+ .clickActionInMore('Manage User')
+ .formTransfer('select_user', username)
+ .formTransferRight('select_user', username)
+ .formSelect('select_user', 'admin')
+ .clickModalActionSubmitButton();
+ });
+ ```
+
+ ![transfer-left](/docs/zh/test/images/e2e/form/transfer-left.png)
+
+- `formTransferRight`
+ - 对右侧的穿梭框基于搜索展示指定待选条目
+ - 参数`formItemName`,即开发代码中`formItem`的`name`值
+ - 参数`value`,搜索内容
+ - 以用户组管理用户为例`test/e2e/integration/pages/identity/user-group.spec.js`为例
+
+ ```javascript
+ it('successfully manage user', () => {
+ cy.tableSearchText(name)
+ .clickActionInMore('Manage User')
+ .formTransfer('select_user', username)
+ .formTransferRight('select_user', username)
+ .clickModalActionSubmitButton();
+
+ cy.tableSearchText(name)
+ .goToDetail()
+ .clickDetailTab('Sub User', 'userGroup');
+ });
+ ```
+
+ ![transfer-right](/docs/zh/test/images/e2e/form/transfer-right.png)
+
+- `formTabClick`
+ - 点击带有 Tab 的表单项中的 Tab
+ - 参数`formItemName`,即开发代码中`formItem`的`name`值
+ - 参数`index`,指定的 Tab 的下标
+ - 以编辑浮动 IP,切换到共享策略为例`test/e2e/integration/pages/network/floatingip.spec.js`为例
+
+ ```javascript
+ it('successfully edit', () => {
+ cy.clickFirstActionButton()
+ .formText('description', 'description')
+ .formTabClick('qos_policy_id', 1)
+ .wait(5000)
+ .formTableSelectBySearch('qos_policy_id', policyName)
+ .clickModalActionSubmitButton()
+ .wait(2000);
+ });
+ ```
+
+ ![tab](/docs/zh/test/images/e2e/form/tab.png)
+
+- `formInputKeyValue`
+ - 对`KeyValue`组件的表单项进行输入操作,一般是配合`formAddSelectAdd`使用,对添加的新的`KeyValue`组件的条目,输入内容
+ - 参数`formItemName`,即开发代码中`formItem`的`name`值
+ - 参数`key`,左侧 input 输入的内容
+ - 参数`value`,右侧 input 输入的内容
+ - 以主机集合管理元数据时,添加新的自定义元数据`test/e2e/integration/pages/compute/aggregate.spec.js`为例
+
+ ```javascript
+ it('successfully manage metadata', () => {
+ cy.tableSearchText(name)
+ .clickActionInMore('Manage Metadata')
+ .wait(5000)
+ .formAddSelectAdd('customs')
+ .formInputKeyValue('customs', 'key', 'value')
+ .formTransferLeftCheck('systems', 0)
+ .clickModalActionSubmitButton();
+ });
+ ```
+
+ ![key-value](/docs/zh/test/images/e2e/form/key-value.png)
+
+- `formTransferLeftCheck`
+ - 对左侧的穿梭框的操作
+ 1. 选中左侧穿梭框中的指定节点
+ 2. 点击穿梭框中间的方向按钮,使得选中内容进入到右侧穿梭框中
+ - 参数`formItemName`,即开发代码中`formItem`的`name`值
+ - 参数`index`,节点的下标
+ - 以主机集合管理元数据时,添加新的自定义元数据`test/e2e/integration/pages/compute/aggregate.spec.js`为例
+
+ ```javascript
+ it('successfully manage metadata', () => {
+ cy.tableSearchText(name)
+ .clickActionInMore('Manage Metadata')
+ .wait(5000)
+ .formAddSelectAdd('customs')
+ .formInputKeyValue('customs', 'key', 'value')
+ .formTransferLeftCheck('systems', 0)
+ .clickModalActionSubmitButton();
+ });
+ ```
+
+ ![transfer-left-click](/docs/zh/test/images/e2e/form/transfer-left-click.png)
+
+- `formTransferRightCheck`
+ - 对右侧的穿梭框的操作
+ 1. 选中右侧穿梭框中的节点
+ 2. 点击穿梭框中间的方向按钮,使得选中内容进入到左侧穿梭框中
+ - 参数`formItemName`,即开发代码中`formItem`的`name`值
+ - 参数`index`,节点的下标
+ - 以云主机类型,修改元数据`test/e2e/integration/pages/compute/flavor.spec.js`为例
+
+ ```javascript
+ it('successfully manage metadata', () => {
+ cy.clickTab('Custom')
+ .tableSearchText(customName)
+ .clickActionButtonByTitle('Manage Metadata')
+ .wait(5000)
+ .formTransferLeftCheck('systems', 0)
+ .clickModalActionSubmitButton();
+
+ // todo: remove key-value metadata
+ cy.clickTab('Custom')
+ .tableSearchText(customName)
+ .clickActionButtonByTitle('Manage Metadata')
+ .wait(5000)
+ .formTransferRightCheck('systems', 0)
+ .clickModalActionSubmitButton();
+ });
+ ```
+
+ ![transfer-right-check](/docs/zh/test/images/e2e/form/transfer-right-check.png)
+
+对资源操作的各种操作,主要用到了上方介绍的函数,函数的具体编写,请查看`test/e2e/support/form-commands.js`
diff --git a/docs/zh/test/3-2-E2E-table-operation.md b/docs/zh/test/3-2-E2E-table-operation.md
new file mode 100644
index 00000000..ffca4996
--- /dev/null
+++ b/docs/zh/test/3-2-E2E-table-operation.md
@@ -0,0 +1,548 @@
+简体中文 | [English](/docs/en/test/3-2-E2E-table-operation.md)
+
+因为前端框架使用的一致性,我们在编写表单操作的相关用例,选取元素并进行操作时,往往会发现有很强的规律性,所以我们对大多数表格操作都编写了相应的 Cypress 函数,极大的减少了编写测试用例的难度,以下会对主要使用的表格操作函数做出详细的说明。
+
+## 对表格整体的操作
+
+主要包含:等待列表加载完成
+
+- `waitTableLoading`
+ - 等待列表加载完成
+ - 列表在加载过程中,会有`loading`状态展示,等待该状态结束
+
+ ![wait-table-loading](/docs/zh/test/images/e2e/table/wait-table-loading.png)
+
+- `checkTableFirstRow`
+ - 验证表格第一行是否包含指定内容,一般用于创建后验证创建资源是否存在
+ - 参数`name`,第一行需要包含的内容,一般用于验证名称是否存在
+ - 以查看密钥详情`test/e2e/integration/pages/compute/keypair.spec.js`为例
+ - 创建后,检查密钥是否存在,验证成功后进入详情页
+
+ ```javascript
+ it('successfully detail', () => {
+ cy.tableSearchText(name)
+ .checkTableFirstRow(name)
+ .goToDetail()
+ .checkDetailName(name);
+ cy.goBackToList(listUrl);
+ });
+ ```
+
+ ![check-first-row](/docs/zh/test/images/e2e/table/check-first-row.png)
+
+- `tableSearchText`
+ - 在表格上方的搜索栏中输入内容,并等待搜索完成
+ - 参数`str`,搜索的内容,一般是搜索名称
+ - 通过搜索,使得待操作的资源位于表格中的第一行,以便进行后续的操作
+ - 以查看密钥详情`test/e2e/integration/pages/compute/keypair.spec.js`为例
+ 1. 创建后,使用名称搜索密钥,并等待搜索完成
+ 2. 检查表格中第一行是否包含指定名称的资源
+ 3. 进入详情页,检查名称是否与预期一致
+ 4. 返回列表页
+
+ ```javascript
+ it('successfully detail', () => {
+ cy.tableSearchText(name)
+ .checkTableFirstRow(name)
+ .goToDetail()
+ .checkDetailName(name);
+ cy.goBackToList(listUrl);
+ });
+ ```
+
+ ![search](/docs/zh/test/images/e2e/table/search.png)
+
+- `tableSimpleSearchText`
+ - 在表格上方的搜索栏中输入内容,并等待搜索完成
+ - 有些表格使用的是简单搜索,搜索项只支持输入文字,此时搜索框使用的组件与`tableSearchText`中的搜索框组件不一样
+ - 参数`str`,搜索的内容,一般是搜索名称
+ - 通过搜索,使得待操作的资源位于表格中的第一行,以便进行后续的操作
+ - 以搜索系统信息中的服务`test/e2e/integration/pages/configuration/system.spec.js`为例
+
+ ```javascript
+ it('successfully services', () => {
+ cy.tableSimpleSearchText('nova');
+ });
+ ```
+
+ ![simple-search](/docs/zh/test/images/e2e/table/simple-search.png)
+
+- `tableSearchSelect`
+ - 使用表格上方的搜索栏中的选择项进行搜索,并等待搜索完成
+ 1. 点击输入框,在待选搜索项中选择搜索项
+ 2. 点击选中搜索类别下的选择项
+ 3. 等待搜索完成
+ - 参数`name`,搜索项的名称
+ - 参数`value`,搜索项对应的选择项的标签
+ - 通过搜索,使得待操作的资源位于表格中的第一行,以便进行后续的操作
+ - 以浮动IP绑定云主机`test/e2e/integration/pages/network/floatingip.spec.js`为例
+ 1. 在浮动IP表格中,搜索`状态`为`停止`的浮动IP
+ 2. 对表格中的第一个资源点击`关联`操作
+ 3. 完成绑定云主机的操作
+
+ ```javascript
+ it('successfully associate instance', () => {
+ cy.tableSearchSelect('Status', 'Down')
+ .clickActionInMore('Associate')
+ .wait(5000)
+ .formTableSelectBySearch('instance', instanceName)
+ .wait(5000)
+ .formTableSelect('port')
+ .clickModalActionSubmitButton();
+ });
+ ```
+
+ ![search-select-1](/docs/zh/test/images/e2e/table/search-select-1.png)
+
+ ![search-select-2](/docs/zh/test/images/e2e/table/search-select-2.png)
+
+ ![search-select-3](/docs/zh/test/images/e2e/table/search-select-3.png)
+
+- `tableSearchSelectText`
+ - 使用表格上方的搜索栏进行搜索,并等待搜索完成
+ 1. 点击输入框,在待选搜索项中选择搜索项
+ 2. 输入搜索内容,回车
+ 3. 等待搜索完成
+ - 不选择搜索项时直接输入,是对第一个支持输入的搜索项进行搜索
+ - 参数`name`,搜索项的名称
+ - 参数`value`,输入的内容
+ - 通过搜索,使得待操作的资源位于表格中的第一行,以便进行后续的操作
+ - 以创建堆栈`test/e2e/integration/pages/heat/stack.spec.js`为例
+ 1. 创建后,进入资源列表页
+ 2. 在列表页使用名称搜索
+ 3. 等待资源的状态可用
+
+ ```javascript
+ it('successfully create', () => {
+ const volumeJson = {
+ name: volumeName,
+ };
+ cy.clickHeaderButton(1, 2000)
+ .formAttachFile('content', contentFile)
+ .formAttachFile('params', paramFile)
+ .clickStepActionNextButton()
+ .wait(2000)
+ .formInput('name', name)
+ .formJsonInput('volume_name_spec', volumeJson)
+ .clickStepActionNextButton()
+ .waitFormLoading()
+ .wait(5000)
+ .tableSearchSelectText('Name', name)
+ .waitStatusActiveByRefresh();
+ });
+ ```
+
+ ![search-text-1](/docs/zh/test/images/e2e/table/search-text-1.png)
+
+ ![search-text-2](/docs/zh/test/images/e2e/table/search-text-2.png)
+
+ ![search-text-3](/docs/zh/test/images/e2e/table/search-text-3.png)
+
+- `checkEmptyTable`
+ - 验证表格为空表格
+ - 一般用于删除资源后验证
+ - 以删除路由器`test/e2e/integration/pages/network/router.spec.js`为例
+ 1. 关闭外网网关
+ 2. 删除
+ 3. 搜索
+ 4. 验证表格为空表格,即删除成功
+
+ ```javascript
+ it('successfully close external gateway and delete', () => {
+ cy.tableSearchText(newname)
+ .clickConfirmActionInMore('Close External Gateway')
+ .clickConfirmActionInMore('Delete')
+ .tableSearchText(newname)
+ .checkEmptyTable();
+ });
+ ```
+
+- `goToDetail`
+ - 访问第一行资源的详情页,并等待详情页加载完成
+ - 参数`index`,链接所在列的下标,默认为`1`
+ - 参数`waitTime`,详情页加载完后等待的时间
+ - 以镜像`test/e2e/integration/pages/compute/image.spec.js`为例
+ 1. 搜索
+ 2. 进入详情页
+ 3. 验证详情名称
+ 4. 返回列表页
+
+ ```javascript
+ it('successfully detail', () => {
+ cy.tableSearchText(name).goToDetail();
+ cy.checkDetailName(name);
+ cy.goBackToList(listUrl);
+ });
+ ```
+
+ ![detail-1](/docs/zh/test/images/e2e/table/detail-1.png)
+
+ ![detail-2](/docs/zh/test/images/e2e/table/detail-2.png)
+
+- `checkColumnValue`
+ - 验证第一行指定列的内容是否符合预期
+ - 参数`columnIndex`,指定列的下标
+ - 参数`value`,预期的值
+ - 以云主机`test/e2e/integration/pages/compute/image.spec.js`为例
+ 1. 搜索
+ 2. 关闭云主机
+ 3. 验证云主机的状态为`关闭`
+ 4. 验证批量操作中的`关闭`操作不可用
+
+ ```javascript
+ it('successfully stop', () => {
+ cy.tableSearchText(name)
+ .clickConfirmActionInMoreSub('Stop', 'Instance Status')
+ .wait(10000)
+ .tableSearchText(name)
+ .checkColumnValue(6, 'Shutoff')
+ .selectFirst()
+ .clickHeaderButtonByTitle('Stop')
+ .checkDisableAction(2000);
+ });
+ ```
+
+ ![check-value](/docs/zh/test/images/e2e/table/check-value.png)
+
+- `selectFirst`
+ - 选中表格中第一行,以便做后续的批量操作
+ - 以云主机`test/e2e/integration/pages/compute/image.spec.js`为例
+ 1. 搜索
+ 2. 关闭云主机
+ 3. 验证云主机的状态为`关闭`
+ 4. 选中第一行
+ 5. 点击批量操作中的`关闭`按钮
+ 6. 弹出错误提示
+
+ ```javascript
+ it('successfully stop', () => {
+ cy.tableSearchText(name)
+ .clickConfirmActionInMoreSub('Stop', 'Instance Status')
+ .wait(10000)
+ .tableSearchText(name)
+ .checkColumnValue(6, 'Shutoff')
+ .selectFirst()
+ .clickHeaderButtonByTitle('Stop')
+ .checkDisableAction(2000);
+ });
+ ```
+
+ ![select-first](/docs/zh/test/images/e2e/table/select-first.png)
+
+- `selectAll`
+ - 选中表格中所有条目,以便做后续的批量操作
+ - 通常是为了清空数据使用
+
+- `waitStatusActiveByRefresh`
+ - 每`5`秒点击表格上方的刷新按钮,直到资源状态变为可用状态
+ - 资源在创建或变更后,往往需要一定的时间才能变为可用状态,之后才能进行后续操作
+ - 以创建堆栈`test/e2e/integration/pages/heat/stack.spec.js`为例
+
+ ```javascript
+ it('successfully create', () => {
+ const volumeJson = {
+ name: volumeName,
+ };
+ cy.clickHeaderButton(1, 2000)
+ .formAttachFile('content', contentFile)
+ .formAttachFile('params', paramFile)
+ .clickStepActionNextButton()
+ .wait(2000)
+ .formInput('name', name)
+ .formJsonInput('volume_name_spec', volumeJson)
+ .clickStepActionNextButton()
+ .waitFormLoading()
+ .wait(5000)
+ .tableSearchSelectText('Name', name)
+ .waitStatusActiveByRefresh();
+ });
+ ```
+
+ ![wait-1](/docs/zh/test/images/e2e/table/wait-1.png)
+
+ ![wait-2](/docs/zh/test/images/e2e/table/wait-2.png)
+
+## 对按钮的操作
+
+主要包含
+ - 位于表单上放的主按钮操作(一般创建操作)、批量操作
+ - 位于表单每一行的行操作
+### 表单上方按钮的操作
+
+表格上方的按钮一般包含:刷新、创建、批量操作按钮、配置表格列表项、下载
+- `clickHeaderButton`
+ - 点击表格上方的按钮,
+ - 参数`buttonIndex`,表格上方按钮的下标
+ - 参数`waitTime`,点击后的等待时间,默认为 2 秒
+ - 一般,创建按钮的下标是 1
+ - 以创建密钥用例`test/e2e/integration/pages/compute/keypair.spec.js`为例
+
+ ```javascript
+ it('successfully create', () => {
+ cy.clickHeaderButton(1)
+ .formInput('name', name)
+ .clickModalActionSubmitButton()
+ .wait(5000);
+ });
+ ```
+
+ ![header-btn-index](/docs/zh/test/images/e2e/table/header-btn-index.png)
+
+- `clickHeaderButtonByTitle`
+ - 通过名称点击表格上方的按钮,一般用于批量操作按钮的点击
+ - 参数`title`,表格上方按钮上的文字
+ - 参数`waitTime`,点击后的等待时间,默认为 2 秒
+ - 以关闭状态下的云主机无法进行关闭操作`test/e2e/integration/pages/compute/instance.spec.js`为例
+ - 点击表单顶部的关闭按钮
+
+ ```javascript
+ it('successfully stop', () => {
+ cy.tableSearchText(name)
+ .clickConfirmActionInMoreSub('Stop', 'Instance Status')
+ .wait(10000)
+ .tableSearchText(name)
+ .checkColumnValue(6, 'Shutoff')
+ .selectFirst()
+ .clickHeaderButtonByTitle('Stop')
+ .checkDisableAction(2000);
+ });
+ ```
+
+ ![header-btn-title](/docs/zh/test/images/e2e/table/header-btn-title.png)
+
+- `clickHeaderConfirmButtonByTitle`
+ - 该函数会完成
+ 1. 通过名称点击表格上方的按钮,页面弹出确认操作提示
+ 2. 点击`确认`按钮
+ - 参数`title`,表格上方按钮上的文字
+ - 参数`waitTime`,点击后的等待时间,默认为 2 秒
+ - 以释放浮动IP`test/e2e/integration/pages/network/floatingip.spec.js`为例
+ - 全选停止状态的浮动IP,并批量释放
+
+ ```javascript
+ it('successfully delete', () => {
+ cy.tableSearchSelect('Status', 'Down')
+ .selectAll()
+ .clickHeaderConfirmButtonByTitle('Release');
+ });
+ ```
+
+ ![header-confirm-title](/docs/zh/test/images/e2e/table/header-confirm-title.png)
+
+### 表单第一行的行操作
+
+- `clickFirstActionButton`
+ - 点击表单第一行的操作列中的第一个按钮,一般用于对弹窗型操作按钮
+ 单页型操作按钮的点击
+ - 以编辑用户`test/e2e/integration/pages/identity/user.spec.js`为例
+
+ ```javascript
+ it('successfully edit', () => {
+ cy.tableSearchText(name)
+ .clickFirstActionButton()
+ .formInput('name', newname)
+ .clickModalActionSubmitButton();
+ });
+ ```
+
+ ![click-first](/docs/zh/test/images/e2e/table/click-first.png)
+
+- `clickActionButtonByTitle`
+ - 根据标题点击第一行中的操作
+ - 以编辑、启动服务`test/e2e/integration/pages/configuration/system.spec.js`为例
+ - 当服务启动时,点击`禁用`按钮
+ - 当服务停止时,点击`启用`按钮
+
+ ```javascript
+ it('successfully disable compute services', () => {
+ cy.clickTab(computeServicesTab)
+ .tableSearchText('nova-compute')
+ .clickActionButtonByTitle('Disable')
+ .formText('disabled_reason', reason)
+ .clickModalActionSubmitButton();
+ });
+
+ it('successfully enable compute services', () => {
+ cy.clickTab(computeServicesTab)
+ .tableSearchSelect('Service Status', 'Disabled')
+ .clickActionButtonByTitle('Enable')
+ .clickConfirmActionSubmitButton();
+ });
+ ```
+
+ ![action-by-title](/docs/zh/test/images/e2e/table/action-by-title.png)
+
+ ![action-by-title-2](/docs/zh/test/images/e2e/table/action-by-title-2.png)
+
+- `clickActionInMore`
+ - 根据标题点击第一行中`更多`中的操作
+ - 以点击镜像的创建云主机按钮`test/e2e/integration/pages/compute/image.spec.js`为例
+
+ ```javascript
+ it('successfully create instance with cancel', () => {
+ cy.tableSearchText(name)
+ .clickActionInMore('Create Instance')
+ .wait(2000)
+ .clickStepActionCancelButton();
+ });
+ ```
+
+ ![action-in-more](/docs/zh/test/images/e2e/table/action-in-more.png)
+
+- `clickActionInMoreSub`
+ - 根据标题点击第一行操作的子菜单下的操作
+ - 参数`title`,按钮的标题
+ - 参数`subMenu`,子菜单的标题
+ - 以云主机点击`关联资源`下的`挂载网卡` `test/e2e/integration/pages/compute/image.spec.js`为例
+
+ ```javascript
+ it('successfully attach interface', () => {
+ cy.tableSearchText(name)
+ .clickActionInMoreSub('Attach Interface', 'Related Resources')
+ .wait(5000)
+ .formTableSelect('network')
+ .clickModalActionSubmitButton();
+ });
+ ```
+
+ ![action-in-sub](/docs/zh/test/images/e2e/table/action-in-sub.png)
+
+- `checkActionDisabledInFirstRow`
+ - 验证指定名称的资源的指定操作不可用
+ 1. 基于指定名称搜索资源
+ 2. 验证搜索结果的第一行中的操作列`更多`中不存在指定操作
+ - 参数`title`,操作的名称
+ - 参数`name`,资源的名称
+ - 资源处于某些状态后,某些操作是需要被禁用的,行操作列表中的第一个操作,如果不可操作,则处于`禁用`状态,而`更多`中的操作,如果不可用,则不展示
+ - 以路由器`test/e2e/integration/pages/network/router.spec.js`为例
+ 1. 创建路由器时开启公网网关
+ 2. 验证路由器不可删除,即不存在`删除`按钮
+
+ ```javascript
+ it('successfully disable delete', () => {
+ cy.checkActionDisabledInFirstRow('Delete', name);
+ });
+ ```
+
+ ![disable-more-action](/docs/zh/test/images/e2e/table/disable-more-action.png)
+
+- `clickFirstActionDisabled`
+ - 验证表格中第一行的操作中的第一个操作不可用
+ - 资源处于某些状态后,某些操作是需要被禁用的,行操作列表中的第一个操作,如果不可操作,则处于`禁用`状态,而`更多`中的操作,如果不可用,则不展示
+ - 以云主机组`test/e2e/integration/pages/compute/server-group.spec.js`为例
+ 1. 在云主机组下创建云主机
+ 2. 验证含有云主机的云主机不可删除
+ 3. 删除云主机后,云主机组删除成功
+
+ ```javascript
+ it('successfully delete', () => {
+ cy.clickFirstActionDisabled();
+ cy.forceDeleteInstance(instanceName);
+ cy.wait(5000);
+ cy.visitPage(listUrl)
+ .tableSearchText(name)
+ .clickConfirmActionInFirst()
+ .checkEmptyTable();
+ });
+ ```
+
+ ![disable-first](/docs/zh/test/images/e2e/table/disable-first.png)
+
+- `clickConfirmActionInFirst`
+ - 完成表格中第一行的第一个操作按钮对应的操作
+ 1. 点击表格中第一行的第一个操作按钮,该操作是一个确认型操作
+ 2. 点击`确认`按钮,并等待请求完成,关闭请求成功的提示信息
+ - 参数`waitTime`,关闭操作成功提示后的等待时间
+ - 以云主机组`test/e2e/integration/pages/compute/server-group.spec.js`为例
+ 1. 在云主机组下创建云主机
+ 2. 验证含有云主机的云主机不可删除
+ 3. 删除云主机后,云主机组删除成功
+
+ ```javascript
+ it('successfully delete', () => {
+ cy.clickFirstActionDisabled();
+ cy.forceDeleteInstance(instanceName);
+ cy.wait(5000);
+ cy.visitPage(listUrl)
+ .tableSearchText(name)
+ .clickConfirmActionInFirst()
+ .checkEmptyTable();
+ });
+ ```
+
+ ![first-confirm](/docs/zh/test/images/e2e/table/first-confirm.png)
+
+ ![first-confirm-2](/docs/zh/test/images/e2e/table/first-confirm-2.png)
+
+- `clickConfirmActionButton`
+ - 完成表格中第一行的列出的操作按钮中对应的操作
+ 1. 点击表格中第一行的指定操作,该操作是一个确认型操作
+ 2. 点击`确认`按钮,并等待请求完成,关闭请求成功的提示信息
+ - 参数`title`,指定操作的名称
+ - 参数`waitTime`,关闭操作提示成功后的等待时间
+ - 以删除VPN IPsec策略`test/e2e/integration/pages/compute/server-group.spec.js`为例
+
+ ```javascript
+ it('successfully delete ipsec policy', () => {
+ cy.clickTab('IPsec Policy')
+ .tableSearchText(ipsecPolicy)
+ .clickConfirmActionButton('Delete');
+ });
+ ```
+
+ ![confirm-action](/docs/zh/test/images/e2e/table/confirm-action.png)
+
+- `clickConfirmActionInMore`
+ - 完成表格中第一行的`更多`中对应的操作
+ 1. 点击表格中第一行的`更多`中指定操作,该操作是一个确认型操作
+ 2. 点击`确认`按钮,并等待请求完成,关闭请求成功的提示信息
+ - 参数`title`,指定操作的名称
+ - 参数`waitTime`,关闭操作提示成功后的等待时间
+ - 以删除路由器`test/e2e/integration/pages/network/router.spec.js`为例
+ 1. 搜索
+ 2. 完成`更多`中的`关闭公网网关`操作
+ 2. 完成`更多`中的`删除`操作
+
+ ```javascript
+ it('successfully close external gateway and delete', () => {
+ cy.tableSearchText(newname)
+ .clickConfirmActionInMore('Close External Gateway')
+ .clickConfirmActionInMore('Delete')
+ .tableSearchText(newname)
+ .checkEmptyTable();
+ });
+ ```
+
+ ![confirm-more-1](/docs/zh/test/images/e2e/table/confirm-more-1.png)
+
+ ![confirm-more-2](/docs/zh/test/images/e2e/table/confirm-more-2.png)
+
+- `clickConfirmActionInMoreSub`
+ - 完成表格中第一行的`更多`中指定子菜单下对应的操作
+ 1. 点击表格中第一行的`更多`中指定子菜单下的指定操作,该操作是一个确认型操作
+ 2. 点击`确认`按钮,并等待请求完成,关闭请求成功的提示信息
+ - 参数`title`,指定操作的名称
+ - 参数`subMenu`,指定子菜单的名称
+ - 参数`waitTime`,关闭操作提示成功后的等待时间
+ - 以锁定云主机`test/e2e/integration/pages/compute/instance.spec.js`为例
+
+ ```javascript
+ it('successfully lock', () => {
+ cy.tableSearchText(name)
+ .clickConfirmActionInMoreSub('Lock', 'Instance Status')
+ .wait(10000);
+ cy.tableSearchText(name)
+ .selectFirst()
+ .clickHeaderButtonByTitle('Start')
+ .checkDisableAction(2000)
+ .clickHeaderButtonByTitle('Stop')
+ .checkDisableAction(2000)
+ .clickHeaderButtonByTitle('Reboot')
+ .checkDisableAction(2000);
+ });
+ ```
+
+ ![confirm-in-sub](/docs/zh/test/images/e2e/table/confirm-in-sub.png)
+
+
+对表格操作的各种操作,主要用到了上方介绍的函数,函数的具体编写,请查看`test/e2e/support/table-commands.js`
diff --git a/docs/zh/test/3-3-E2E-detail-operation.md b/docs/zh/test/3-3-E2E-detail-operation.md
new file mode 100644
index 00000000..cf31237b
--- /dev/null
+++ b/docs/zh/test/3-3-E2E-detail-operation.md
@@ -0,0 +1,96 @@
+简体中文 | [English](/docs/en/test/3-3-E2E-detail-operation.md)
+
+因为前端框架使用的一致性,我们在编写详情操作的相关用例,选取元素并进行操作时,往往会发现有很强的规律性,所以我们对大多数详情操作都编写了相应的 Cypress 函数,极大的减少了编写测试用例的难度,以下会对主要使用的表格操作函数做出详细的说明。
+
+- `checkDetailName`
+ - 验证详情页头部包含指定资源名称
+ - 参数`name`,资源名称
+ - 以查看密钥详情`test/e2e/integration/pages/compute/keypair.spec.js`为例
+
+ ```javascript
+ it('successfully detail', () => {
+ cy.tableSearchText(name)
+ .checkTableFirstRow(name)
+ .goToDetail()
+ .checkDetailName(name);
+ cy.goBackToList(listUrl);
+ });
+ ```
+
+ ![name](/docs/zh/test/images/e2e/detail/name.png)
+
+- `goBackToList`
+ - 点击详情页的`返回`按钮,进入列表页,并等待列表加载完成
+ - 参数`url`,列表url
+ - 如果设置,会验证返回的列表路由是否符合预期
+ - 以查看密钥详情`test/e2e/integration/pages/compute/keypair.spec.js`为例
+ 1. 搜索
+ 2. 验证表格第一行是否包含指定名称
+ 3. 进入详情页
+ 4. 验证详情页的名称
+ 5. 返回列表页
+
+ ```javascript
+ it('successfully detail', () => {
+ cy.tableSearchText(name)
+ .checkTableFirstRow(name)
+ .goToDetail()
+ .checkDetailName(name);
+ cy.goBackToList(listUrl);
+ });
+ ```
+
+ ![list](/docs/zh/test/images/e2e/detail/list.png)
+
+- `goBackToList`
+ - 点击详情页的`返回`按钮,进入列表页,并等待列表加载完成
+ - 参数`url`,列表url
+ - 如果设置,会验证返回的列表路由是否符合预期
+ - 以查看密钥详情`test/e2e/integration/pages/compute/keypair.spec.js`为例
+ 1. 搜索
+ 2. 验证表格第一行是否包含指定名称
+ 3. 进入详情页
+ 4. 验证详情页的名称
+ 5. 返回列表页
+
+ ```javascript
+ it('successfully detail', () => {
+ cy.tableSearchText(name)
+ .checkTableFirstRow(name)
+ .goToDetail()
+ .checkDetailName(name);
+ cy.goBackToList(listUrl);
+ });
+ ```
+
+ ![list](/docs/zh/test/images/e2e/detail/list.png)
+
+- `clickDetailTab`
+ - 点击详情页下方的指定Tab标签,并等待相关资源列表加载完成
+ - 参数`label`,指定的Tab标签
+ - 参数`urlTab`,路由中的tab属性
+ - 如果设置,会验证切换标签后路由中的tab属性是否符合预期
+ - 参数`waitTime`,切换标签后的等待时间
+ - 以查看网络详情`test/e2e/integration/pages/network/network.spec.js`为例
+ 1. 搜索
+ 2. 验证表格第一行是否包含指定名称
+ 3. 进入详情页
+ 4. 验证详情页的名称
+ 5. 点击子网Tab,并等待列表加载完成
+ 6. 点击端口Tab,并等待列表加载完成
+ 5. 返回列表页
+
+ ```javascript
+ it('successfully detail', () => {
+ cy.tableSearchText(name)
+ .checkTableFirstRow(name)
+ .goToDetail()
+ .checkDetailName(name);
+ cy.clickDetailTab('Subnets', 'subnets').clickDetailTab('Ports', 'ports');
+ cy.goBackToList(listUrl);
+ });
+ ```
+
+ ![tab](/docs/zh/test/images/e2e/detail/tab.png)
+
+对详情页主要用到了上方介绍的函数,函数的具体编写,请查看`test/e2e/support/detail-commands.js`
\ No newline at end of file
diff --git a/docs/zh/test/3-4-E2E-resource-operation.md b/docs/zh/test/3-4-E2E-resource-operation.md
new file mode 100644
index 00000000..65cbd864
--- /dev/null
+++ b/docs/zh/test/3-4-E2E-resource-operation.md
@@ -0,0 +1,276 @@
+简体中文 | [English](/docs/en/test/3-4-E2E-resource-operation.md)
+
+在E2E的过程中,创建资源的时候,往往需要先创建关联资源,而删除资源后,也需要删除掉相关资源,所以以完整创建/删除为原则,封装了对相关资源的操作。
+
+- `createInstance`
+ - 创建云主机,并等待云主机变为`运行中`状态
+ - 参数`name`,云主机的名称
+ - 参数`networkName`,云主机创建时选择的网络名称
+ - 以浮动IP关联云主机`test/e2e/integration/pages/network/floatingip.spec.js`为例
+ - 为了能成功关联云主机,需要满足云主机网卡所在的子网所连接的路由器开启了公网网关
+ 1. 创建带有子网的网络`networkName`
+ 2. 创建开启了公网网关并连接网络`networkName`子网的路由器`routerName`
+ 3. 创建挂载了网络`networkName`上的网卡的云主机`instanceName`
+
+ ```javascript
+ it('successfully prepair resource', () => {
+ cy.createNetwork({ name: networkName });
+ cy.createRouter({ name: routerName, network: networkName });
+ cy.createInstance({ name: instanceName, networkName });
+ });
+ ```
+
+- `createNetwork`
+ - 创建网络,该网络带有一个子网
+ - 参数`name`,网络的名称
+ - 参数`networkName`,云主机创建时选择的网络名称
+ - 以路由器连接子网为例`test/e2e/integration/pages/network/router.spec.js`为例
+ - 创建了名称为`networkName`的网络,为连接子网做准备
+
+ ```javascript
+ it('successfully prepair resource', () => {
+ cy.createNetwork({ name: networkName });
+ });
+ ```
+
+- `createNetworkPolicy`
+ - 创建网络QoS策略
+ - 参数`name`,策略的名称
+ - 以虚拟网卡修改QoS为例`test/e2e/integration/pages/network/virtual-adapter.spec.js`为例
+ - 创建了名称为`policyName`的策略,为修改QoS做准备
+
+ ```javascript
+ it('successfully prepair resource by admin', () => {
+ cy.loginAdmin().wait(5000).createNetworkPolicy({ name: policyName });
+ });
+ ```
+
+- `createRouter`
+ - 创建开启了公网网关的路由器
+ - 参数`name`,路由器的名称
+ - 参数`network`
+ - 若设置,则路由器会连接`network`网络的子网
+ - 以浮动IP关联云主机`test/e2e/integration/pages/network/floatingip.spec.js`为例
+ - 为了能成功关联云主机,需要满足云主机网卡所在的子网所连接的路由器开启了公网网关
+ 1. 创建带有子网的网络`networkName`
+ 2. 创建开启了公网网关并连接网络`networkName`子网的路由器`routerName`
+ 3. 创建挂载了网络`networkName`上的网卡的云主机`instanceName`
+
+ ```javascript
+ it('successfully prepair resource', () => {
+ cy.createNetwork({ name: networkName });
+ cy.createRouter({ name: routerName, network: networkName });
+ cy.createInstance({ name: instanceName, networkName });
+ });
+ ```
+
+- `deleteRouter`
+ - 删除路由器,会断开路由器的子网,关闭路由器的公网网关,最终成功删除路由器
+ - 参数`network`
+ - 若设置,则需要先断开路由器的子网
+ - 参数`name`,路由器的名称
+
+ - 以浮动IP删除关联资源`test/e2e/integration/pages/network/floatingip.spec.js`为例
+ - 为了能成功关联云主机,需要满足云主机网卡所在的子网所连接的路由器开启了公网网关
+
+ ```javascript
+ it('successfully delete related resources', () => {
+ cy.forceDeleteInstance(instanceName);
+ cy.deleteRouter(routerName, networkName);
+ cy.deleteAll('network', networkName);
+ cy.loginAdmin().wait(5000);
+ cy.deleteAll('networkQosPolicy', policyName);
+ });
+ ```
+
+- `forceDeleteInstance`
+ - 强制删除云主机,而不是使用软删除
+ - 参数`name`,云主机的名称
+ - 以删除云主机组`test/e2e/integration/pages/compute/server-group.spec.js`为例
+ 1. 先删除云主机组下的云主机
+ 2. 再成功删除云主机组
+
+ ```javascript
+ it('successfully delete', () => {
+ cy.clickFirstActionDisabled();
+ cy.forceDeleteInstance(instanceName);
+ cy.wait(5000);
+ cy.visitPage(listUrl)
+ .tableSearchText(name)
+ .clickConfirmActionInFirst()
+ .checkEmptyTable();
+ });
+ ```
+
+- `createVolume`
+ - 创建云硬盘
+ - 参数`name`,云硬盘的名称
+ - 以云硬盘备份`test/e2e/integration/pages/storage/backup.spec.js`为例
+ - 创建云硬盘的备份,需要先准备好云硬盘
+
+ ```javascript
+ it('successfully prepair resource', () => {
+ cy.createVolume(volumeName);
+ cy.createNetwork({ name: networkName });
+ cy.createInstance({ name: instanceName, networkName });
+ });
+ ```
+
+- `createSecurityGrouop`
+ - 创建安全组
+ - 参数`name`,安全组的名称
+ - 以虚拟网卡`test/e2e/integration/pages/network/virtual-adapter.spec.js`为例
+ - 测试管理安全组,需要先准备好安全组
+
+ ```javascript
+ it('successfully prepair resource', () => {
+ cy.createFip();
+ cy.createSecurityGrouop({ name: securityGroupName });
+ cy.createNetwork({ name: networkName });
+ cy.createRouter({ name: routerName, network: networkName });
+ cy.createInstance({ name: instanceName, networkName });
+ });
+ ```
+
+- `createFip`
+ - 创建浮动IP
+ - 以云主机`test/e2e/integration/pages/compute/instance.spec.js`为例
+ - 测试绑定浮动IP,需要准备好可达的浮动IP
+
+ ```javascript
+ it('successfully prepair resource', () => {
+ cy.createNetwork({ name: networkName });
+ cy.createRouter({ name: routerName, network: networkName });
+ cy.createFip();
+ cy.createVolume(volumeName);
+ });
+ ```
+
+- `createUserGroup`
+ - 创建用户组
+ - 参数`name`,用户组的名称
+ - 以项目`test/e2e/integration/pages/identity/project.spec.js`为例
+ - 测试管理用户组操作,需要准备好用户组
+
+ ```javascript
+ it('successfully prepair resource', () => {
+ cy.createUser({ name: username });
+ cy.createUserGroup({ name: userGroupName });
+ });
+ ```
+
+- `createUser`
+ - 创建用户
+ - 参数`name`,用户的名称
+ - 以项目`test/e2e/integration/pages/identity/project.spec.js`为例
+ - 测试管理用户操作,需要准备好用户
+
+ ```javascript
+ it('successfully prepair resource', () => {
+ cy.createUser({ name: username });
+ cy.createUserGroup({ name: userGroupName });
+ });
+ ```
+
+- `createProject`
+ - 创建项目
+ - 参数`name`,项目的名称
+ - 以用户`test/e2e/integration/pages/identity/user.spec.js`为例
+ - 测试创建用户,需要准备项目
+ - 测试管理项目权限,需要准备项目
+
+ ```javascript
+ it('successfully prepair resource', () => {
+ cy.createProject({ name: projectName });
+ cy.createProject({ name: projectName2 });
+ cy.createUserGroup({ name: userGroupName });
+ });
+ ```
+
+- `createIronicImage`
+ - 创建裸机使用的镜像
+ - 参数`name`,镜像的名称
+ - 以裸机`test/e2e/integration/pages/compute/ironic.spec.js`为例
+ - 创建裸机,需要能创建裸机的镜像
+
+ ```javascript
+ it('successfully prepair resource', () => {
+ cy.createNetwork({ name: networkName });
+ cy.createRouter({ name: routerName, network: networkName });
+ cy.createFip();
+ cy.createIronicImage({ name: imageName });
+ });
+ ```
+
+- `deleteInstance`
+ - 删除云主机
+ - 参数`name`,云主机的名称
+ - 参数`deleteRecycleBin`,默认为`true`,表示需要进入回收站二次删除
+ - 以云主机删除`test/e2e/integration/pages/compute/instance.spec.js`为例
+
+ ```javascript
+ it('successfully delete', () => {
+ cy.deleteInstance(newname);
+ });
+ ```
+
+- `deleteAllAvailableVolume`
+ - 删除所有可用的云硬盘
+ - 以云主机`test/e2e/integration/pages/compute/instance.spec.js`为例
+
+ ```javascript
+ it('successfully delete related resources', () => {
+ cy.deleteAll('fip');
+ cy.deleteRouter(routerName, networkName);
+ cy.deleteAll('network', networkName);
+ cy.deleteAll('volume', volumeName);
+ cy.deleteAllAvailableVolume();
+ });
+ ```
+
+- `deleteAll`
+ - 删除符合条件的资源
+ - 参数`resourceName`,资源名称,支持
+
+ ```javacript
+ export default {
+ // compute
+ instance: instanceListUrl,
+ image: imageListUrl,
+
+ // storage
+ volume: volumeListUrl,
+ volumeSnapshot: volumeSnapshotListUrl,
+ backup: backupListUrl,
+ volumeType: volumeTypeListUrl,
+
+ // network
+ network: networkListUrl,
+ router: routerListUrl,
+ networkQosPolicy: policyListUrl,
+ fip: fipListUrl,
+ virtualAdapter: virtualAdapterListUrl,
+
+ // security
+ securityGroup: securityGroupListUrl,
+
+ // identity
+ project: projectListUrl,
+ user: userListUrl,
+ userGroup: userGroupListUrl,
+ };
+ ```
+
+ - 参数`name`
+ - 如设置,则删除指定名称的资源
+ - 如不设置,则删除资源列表下的所有资源
+ - 参数`tab`
+ - 如设置,表示资源位于`tab`标签下,需要先切换到指定标签下
+ - 以云硬盘类型`test/e2e/integration/pages/storage/volume-type.spec.js`为例
+ - 删除管理QoS时准备的QoS
+
+ ```javascript
+ it('successfully delete related resources', () => {
+ cy.deleteAll('volumeType', qosName, 'QoS');
+ });
+ ```
\ No newline at end of file
diff --git a/docs/zh/test/images/e2e/console.png b/docs/zh/test/images/e2e/console.png
new file mode 100755
index 00000000..c18c58b8
Binary files /dev/null and b/docs/zh/test/images/e2e/console.png differ
diff --git a/docs/zh/test/images/e2e/detail/list.png b/docs/zh/test/images/e2e/detail/list.png
new file mode 100755
index 00000000..5e60b356
Binary files /dev/null and b/docs/zh/test/images/e2e/detail/list.png differ
diff --git a/docs/zh/test/images/e2e/detail/name.png b/docs/zh/test/images/e2e/detail/name.png
new file mode 100755
index 00000000..b6632bbb
Binary files /dev/null and b/docs/zh/test/images/e2e/detail/name.png differ
diff --git a/docs/zh/test/images/e2e/detail/tab.png b/docs/zh/test/images/e2e/detail/tab.png
new file mode 100755
index 00000000..46159ae8
Binary files /dev/null and b/docs/zh/test/images/e2e/detail/tab.png differ
diff --git a/docs/zh/test/images/e2e/form/add-select.png b/docs/zh/test/images/e2e/form/add-select.png
new file mode 100755
index 00000000..bdbbea26
Binary files /dev/null and b/docs/zh/test/images/e2e/form/add-select.png differ
diff --git a/docs/zh/test/images/e2e/form/attach-file-image.png b/docs/zh/test/images/e2e/form/attach-file-image.png
new file mode 100755
index 00000000..fba4f9f9
Binary files /dev/null and b/docs/zh/test/images/e2e/form/attach-file-image.png differ
diff --git a/docs/zh/test/images/e2e/form/attach-file.png b/docs/zh/test/images/e2e/form/attach-file.png
new file mode 100755
index 00000000..ce99003f
Binary files /dev/null and b/docs/zh/test/images/e2e/form/attach-file.png differ
diff --git a/docs/zh/test/images/e2e/form/checkbox.png b/docs/zh/test/images/e2e/form/checkbox.png
new file mode 100755
index 00000000..5f731a88
Binary files /dev/null and b/docs/zh/test/images/e2e/form/checkbox.png differ
diff --git a/docs/zh/test/images/e2e/form/click-confirm-submit.png b/docs/zh/test/images/e2e/form/click-confirm-submit.png
new file mode 100755
index 00000000..4d754fd0
Binary files /dev/null and b/docs/zh/test/images/e2e/form/click-confirm-submit.png differ
diff --git a/docs/zh/test/images/e2e/form/click-form-submit.png b/docs/zh/test/images/e2e/form/click-form-submit.png
new file mode 100755
index 00000000..88a6fbbb
Binary files /dev/null and b/docs/zh/test/images/e2e/form/click-form-submit.png differ
diff --git a/docs/zh/test/images/e2e/form/click-modal-submit.png b/docs/zh/test/images/e2e/form/click-modal-submit.png
new file mode 100755
index 00000000..ff4a3346
Binary files /dev/null and b/docs/zh/test/images/e2e/form/click-modal-submit.png differ
diff --git a/docs/zh/test/images/e2e/form/click-step-next.png b/docs/zh/test/images/e2e/form/click-step-next.png
new file mode 100755
index 00000000..3859687c
Binary files /dev/null and b/docs/zh/test/images/e2e/form/click-step-next.png differ
diff --git a/docs/zh/test/images/e2e/form/disable-action.png b/docs/zh/test/images/e2e/form/disable-action.png
new file mode 100755
index 00000000..ee273fd1
Binary files /dev/null and b/docs/zh/test/images/e2e/form/disable-action.png differ
diff --git a/docs/zh/test/images/e2e/form/form-name.png b/docs/zh/test/images/e2e/form/form-name.png
new file mode 100755
index 00000000..7942dad8
Binary files /dev/null and b/docs/zh/test/images/e2e/form/form-name.png differ
diff --git a/docs/zh/test/images/e2e/form/input.png b/docs/zh/test/images/e2e/form/input.png
new file mode 100755
index 00000000..cb4e085d
Binary files /dev/null and b/docs/zh/test/images/e2e/form/input.png differ
diff --git a/docs/zh/test/images/e2e/form/key-value.png b/docs/zh/test/images/e2e/form/key-value.png
new file mode 100755
index 00000000..d16df1c0
Binary files /dev/null and b/docs/zh/test/images/e2e/form/key-value.png differ
diff --git a/docs/zh/test/images/e2e/form/more-open.png b/docs/zh/test/images/e2e/form/more-open.png
new file mode 100755
index 00000000..d6970761
Binary files /dev/null and b/docs/zh/test/images/e2e/form/more-open.png differ
diff --git a/docs/zh/test/images/e2e/form/more.png b/docs/zh/test/images/e2e/form/more.png
new file mode 100755
index 00000000..4a54b374
Binary files /dev/null and b/docs/zh/test/images/e2e/form/more.png differ
diff --git a/docs/zh/test/images/e2e/form/notice.png b/docs/zh/test/images/e2e/form/notice.png
new file mode 100755
index 00000000..c8b57fd3
Binary files /dev/null and b/docs/zh/test/images/e2e/form/notice.png differ
diff --git a/docs/zh/test/images/e2e/form/radio.png b/docs/zh/test/images/e2e/form/radio.png
new file mode 100755
index 00000000..3d1cb7f9
Binary files /dev/null and b/docs/zh/test/images/e2e/form/radio.png differ
diff --git a/docs/zh/test/images/e2e/form/select-all.png b/docs/zh/test/images/e2e/form/select-all.png
new file mode 100755
index 00000000..b8887e0a
Binary files /dev/null and b/docs/zh/test/images/e2e/form/select-all.png differ
diff --git a/docs/zh/test/images/e2e/form/select-table-option.png b/docs/zh/test/images/e2e/form/select-table-option.png
new file mode 100755
index 00000000..5c6573a3
Binary files /dev/null and b/docs/zh/test/images/e2e/form/select-table-option.png differ
diff --git a/docs/zh/test/images/e2e/form/select-table-search.png b/docs/zh/test/images/e2e/form/select-table-search.png
new file mode 100755
index 00000000..4820c5d9
Binary files /dev/null and b/docs/zh/test/images/e2e/form/select-table-search.png differ
diff --git a/docs/zh/test/images/e2e/form/select-table.png b/docs/zh/test/images/e2e/form/select-table.png
new file mode 100755
index 00000000..e193dc62
Binary files /dev/null and b/docs/zh/test/images/e2e/form/select-table.png differ
diff --git a/docs/zh/test/images/e2e/form/select-value.png b/docs/zh/test/images/e2e/form/select-value.png
new file mode 100755
index 00000000..0968d2d8
Binary files /dev/null and b/docs/zh/test/images/e2e/form/select-value.png differ
diff --git a/docs/zh/test/images/e2e/form/select.png b/docs/zh/test/images/e2e/form/select.png
new file mode 100755
index 00000000..80173c7a
Binary files /dev/null and b/docs/zh/test/images/e2e/form/select.png differ
diff --git a/docs/zh/test/images/e2e/form/switch.png b/docs/zh/test/images/e2e/form/switch.png
new file mode 100755
index 00000000..b5b41f38
Binary files /dev/null and b/docs/zh/test/images/e2e/form/switch.png differ
diff --git a/docs/zh/test/images/e2e/form/tab.png b/docs/zh/test/images/e2e/form/tab.png
new file mode 100755
index 00000000..84e2de86
Binary files /dev/null and b/docs/zh/test/images/e2e/form/tab.png differ
diff --git a/docs/zh/test/images/e2e/form/textarea-json.png b/docs/zh/test/images/e2e/form/textarea-json.png
new file mode 100755
index 00000000..4353baad
Binary files /dev/null and b/docs/zh/test/images/e2e/form/textarea-json.png differ
diff --git a/docs/zh/test/images/e2e/form/transfer-left-click.png b/docs/zh/test/images/e2e/form/transfer-left-click.png
new file mode 100755
index 00000000..7d92f87f
Binary files /dev/null and b/docs/zh/test/images/e2e/form/transfer-left-click.png differ
diff --git a/docs/zh/test/images/e2e/form/transfer-left.png b/docs/zh/test/images/e2e/form/transfer-left.png
new file mode 100755
index 00000000..58a35ca3
Binary files /dev/null and b/docs/zh/test/images/e2e/form/transfer-left.png differ
diff --git a/docs/zh/test/images/e2e/form/transfer-right-check.png b/docs/zh/test/images/e2e/form/transfer-right-check.png
new file mode 100755
index 00000000..2a096b0e
Binary files /dev/null and b/docs/zh/test/images/e2e/form/transfer-right-check.png differ
diff --git a/docs/zh/test/images/e2e/form/transfer-right.png b/docs/zh/test/images/e2e/form/transfer-right.png
new file mode 100755
index 00000000..7f046f60
Binary files /dev/null and b/docs/zh/test/images/e2e/form/transfer-right.png differ
diff --git a/docs/zh/test/images/e2e/form/unselect-all.png b/docs/zh/test/images/e2e/form/unselect-all.png
new file mode 100755
index 00000000..16f94475
Binary files /dev/null and b/docs/zh/test/images/e2e/form/unselect-all.png differ
diff --git a/docs/zh/test/images/e2e/form/wait-form-loading.png b/docs/zh/test/images/e2e/form/wait-form-loading.png
new file mode 100755
index 00000000..b6003db6
Binary files /dev/null and b/docs/zh/test/images/e2e/form/wait-form-loading.png differ
diff --git a/docs/zh/test/images/e2e/gui-list.png b/docs/zh/test/images/e2e/gui-list.png
new file mode 100755
index 00000000..58ea626b
Binary files /dev/null and b/docs/zh/test/images/e2e/gui-list.png differ
diff --git a/docs/zh/test/images/e2e/gui-work.png b/docs/zh/test/images/e2e/gui-work.png
new file mode 100755
index 00000000..32692e40
Binary files /dev/null and b/docs/zh/test/images/e2e/gui-work.png differ
diff --git a/docs/zh/test/images/e2e/result.png b/docs/zh/test/images/e2e/result.png
new file mode 100755
index 00000000..72f1c1bc
Binary files /dev/null and b/docs/zh/test/images/e2e/result.png differ
diff --git a/docs/zh/test/images/e2e/table/action-by-title-2.png b/docs/zh/test/images/e2e/table/action-by-title-2.png
new file mode 100755
index 00000000..c9ad4483
Binary files /dev/null and b/docs/zh/test/images/e2e/table/action-by-title-2.png differ
diff --git a/docs/zh/test/images/e2e/table/action-by-title.png b/docs/zh/test/images/e2e/table/action-by-title.png
new file mode 100755
index 00000000..2ad89cee
Binary files /dev/null and b/docs/zh/test/images/e2e/table/action-by-title.png differ
diff --git a/docs/zh/test/images/e2e/table/action-in-more.png b/docs/zh/test/images/e2e/table/action-in-more.png
new file mode 100755
index 00000000..2d6c3155
Binary files /dev/null and b/docs/zh/test/images/e2e/table/action-in-more.png differ
diff --git a/docs/zh/test/images/e2e/table/action-in-sub.png b/docs/zh/test/images/e2e/table/action-in-sub.png
new file mode 100755
index 00000000..9dcb541a
Binary files /dev/null and b/docs/zh/test/images/e2e/table/action-in-sub.png differ
diff --git a/docs/zh/test/images/e2e/table/check-first-row.png b/docs/zh/test/images/e2e/table/check-first-row.png
new file mode 100755
index 00000000..22a9c2f4
Binary files /dev/null and b/docs/zh/test/images/e2e/table/check-first-row.png differ
diff --git a/docs/zh/test/images/e2e/table/check-value.png b/docs/zh/test/images/e2e/table/check-value.png
new file mode 100755
index 00000000..2e6fee64
Binary files /dev/null and b/docs/zh/test/images/e2e/table/check-value.png differ
diff --git a/docs/zh/test/images/e2e/table/click-first.png b/docs/zh/test/images/e2e/table/click-first.png
new file mode 100755
index 00000000..debac336
Binary files /dev/null and b/docs/zh/test/images/e2e/table/click-first.png differ
diff --git a/docs/zh/test/images/e2e/table/confirm-action.png b/docs/zh/test/images/e2e/table/confirm-action.png
new file mode 100755
index 00000000..6676b4e4
Binary files /dev/null and b/docs/zh/test/images/e2e/table/confirm-action.png differ
diff --git a/docs/zh/test/images/e2e/table/confirm-in-sub.png b/docs/zh/test/images/e2e/table/confirm-in-sub.png
new file mode 100755
index 00000000..982ecb7c
Binary files /dev/null and b/docs/zh/test/images/e2e/table/confirm-in-sub.png differ
diff --git a/docs/zh/test/images/e2e/table/confirm-more-1.png b/docs/zh/test/images/e2e/table/confirm-more-1.png
new file mode 100755
index 00000000..d8be02d8
Binary files /dev/null and b/docs/zh/test/images/e2e/table/confirm-more-1.png differ
diff --git a/docs/zh/test/images/e2e/table/confirm-more-2.png b/docs/zh/test/images/e2e/table/confirm-more-2.png
new file mode 100755
index 00000000..4e0cd9c1
Binary files /dev/null and b/docs/zh/test/images/e2e/table/confirm-more-2.png differ
diff --git a/docs/zh/test/images/e2e/table/detail-1.png b/docs/zh/test/images/e2e/table/detail-1.png
new file mode 100755
index 00000000..a4a2c771
Binary files /dev/null and b/docs/zh/test/images/e2e/table/detail-1.png differ
diff --git a/docs/zh/test/images/e2e/table/detail-2.png b/docs/zh/test/images/e2e/table/detail-2.png
new file mode 100755
index 00000000..4d442942
Binary files /dev/null and b/docs/zh/test/images/e2e/table/detail-2.png differ
diff --git a/docs/zh/test/images/e2e/table/disable-first.png b/docs/zh/test/images/e2e/table/disable-first.png
new file mode 100755
index 00000000..71d78bfd
Binary files /dev/null and b/docs/zh/test/images/e2e/table/disable-first.png differ
diff --git a/docs/zh/test/images/e2e/table/disable-more-action.png b/docs/zh/test/images/e2e/table/disable-more-action.png
new file mode 100755
index 00000000..85fc255a
Binary files /dev/null and b/docs/zh/test/images/e2e/table/disable-more-action.png differ
diff --git a/docs/zh/test/images/e2e/table/first-confirm-2.png b/docs/zh/test/images/e2e/table/first-confirm-2.png
new file mode 100755
index 00000000..5579cfd5
Binary files /dev/null and b/docs/zh/test/images/e2e/table/first-confirm-2.png differ
diff --git a/docs/zh/test/images/e2e/table/first-confirm.png b/docs/zh/test/images/e2e/table/first-confirm.png
new file mode 100755
index 00000000..d2f112d5
Binary files /dev/null and b/docs/zh/test/images/e2e/table/first-confirm.png differ
diff --git a/docs/zh/test/images/e2e/table/header-btn-index.png b/docs/zh/test/images/e2e/table/header-btn-index.png
new file mode 100755
index 00000000..47fab58d
Binary files /dev/null and b/docs/zh/test/images/e2e/table/header-btn-index.png differ
diff --git a/docs/zh/test/images/e2e/table/header-btn-title.png b/docs/zh/test/images/e2e/table/header-btn-title.png
new file mode 100755
index 00000000..bbd146de
Binary files /dev/null and b/docs/zh/test/images/e2e/table/header-btn-title.png differ
diff --git a/docs/zh/test/images/e2e/table/header-confirm-title.png b/docs/zh/test/images/e2e/table/header-confirm-title.png
new file mode 100755
index 00000000..839af1f7
Binary files /dev/null and b/docs/zh/test/images/e2e/table/header-confirm-title.png differ
diff --git a/docs/zh/test/images/e2e/table/search-select-1.png b/docs/zh/test/images/e2e/table/search-select-1.png
new file mode 100755
index 00000000..4602dcea
Binary files /dev/null and b/docs/zh/test/images/e2e/table/search-select-1.png differ
diff --git a/docs/zh/test/images/e2e/table/search-select-2.png b/docs/zh/test/images/e2e/table/search-select-2.png
new file mode 100755
index 00000000..48d27e65
Binary files /dev/null and b/docs/zh/test/images/e2e/table/search-select-2.png differ
diff --git a/docs/zh/test/images/e2e/table/search-select-3.png b/docs/zh/test/images/e2e/table/search-select-3.png
new file mode 100755
index 00000000..07035e60
Binary files /dev/null and b/docs/zh/test/images/e2e/table/search-select-3.png differ
diff --git a/docs/zh/test/images/e2e/table/search-text-1.png b/docs/zh/test/images/e2e/table/search-text-1.png
new file mode 100755
index 00000000..b04507a2
Binary files /dev/null and b/docs/zh/test/images/e2e/table/search-text-1.png differ
diff --git a/docs/zh/test/images/e2e/table/search-text-2.png b/docs/zh/test/images/e2e/table/search-text-2.png
new file mode 100755
index 00000000..1e74f9e3
Binary files /dev/null and b/docs/zh/test/images/e2e/table/search-text-2.png differ
diff --git a/docs/zh/test/images/e2e/table/search-text-3.png b/docs/zh/test/images/e2e/table/search-text-3.png
new file mode 100755
index 00000000..575d4402
Binary files /dev/null and b/docs/zh/test/images/e2e/table/search-text-3.png differ
diff --git a/docs/zh/test/images/e2e/table/search.png b/docs/zh/test/images/e2e/table/search.png
new file mode 100755
index 00000000..61ec8832
Binary files /dev/null and b/docs/zh/test/images/e2e/table/search.png differ
diff --git a/docs/zh/test/images/e2e/table/select-first.png b/docs/zh/test/images/e2e/table/select-first.png
new file mode 100755
index 00000000..a389f302
Binary files /dev/null and b/docs/zh/test/images/e2e/table/select-first.png differ
diff --git a/docs/zh/test/images/e2e/table/simple-search.png b/docs/zh/test/images/e2e/table/simple-search.png
new file mode 100755
index 00000000..4b5d3079
Binary files /dev/null and b/docs/zh/test/images/e2e/table/simple-search.png differ
diff --git a/docs/zh/test/images/e2e/table/wait-1.png b/docs/zh/test/images/e2e/table/wait-1.png
new file mode 100755
index 00000000..e17ee3a9
Binary files /dev/null and b/docs/zh/test/images/e2e/table/wait-1.png differ
diff --git a/docs/zh/test/images/e2e/table/wait-2.png b/docs/zh/test/images/e2e/table/wait-2.png
new file mode 100755
index 00000000..922ba39f
Binary files /dev/null and b/docs/zh/test/images/e2e/table/wait-2.png differ
diff --git a/docs/zh/test/images/e2e/table/wait-table-loading.png b/docs/zh/test/images/e2e/table/wait-table-loading.png
new file mode 100755
index 00000000..e583daeb
Binary files /dev/null and b/docs/zh/test/images/e2e/table/wait-table-loading.png differ
diff --git a/docs/zh/test/images/unit/result.png b/docs/zh/test/images/unit/result.png
new file mode 100755
index 00000000..5c77f001
Binary files /dev/null and b/docs/zh/test/images/unit/result.png differ