首页 前端知识 三、以user表为例,用Amis Sails实现增删改查操作

三、以user表为例,用Amis Sails实现增删改查操作

2024-06-18 23:06:01 前端知识 前端哥 34 66 我要收藏

文章目录

  • CRUD 组件
    • 查询api
    • 分页
      • fetcher参数观察
      • 统一处理method
      • 分页参数提交到后端
      • 自定义分页和页面大小(pageSize)
    • 搜索
    • 排序
    • 头部工具条
      • 列折叠按钮
      • 刷新和导出excel
      • 自定义内容
    • 单条删除
    • 批量删除
    • 新增数据
    • headerToolbar 结果分析
    • 前端数据格式要求
    • 数据修改
    • 重组提交数据
  • 修改左边菜单

CRUD 组件

CRUD是增加(Create)、读取(Read)、更新(Update)和删除(Delete)的意思。Amis 的CRUD 即增删改查组件,主要用来展现数据列表,并支持各类【增】【删】【改】【查】等操作。
一个最简单的CRUD组件的Schema代码如下:

{//第一对大括号代表Amis组件的根节点rootNode
  "type": "page",//根节点的类型是page
  "body": {//根节点的body内容
    "type": "crud",//根节点的body类型是crud
    "api": "/amis/api/mock2/sample",//crud 组件的api
    "columns": [//curd组件的列数据(相当于数据库中的字段信息数据)
      {
        "name": "id",//对应数据库中的字段名称
        "label": "ID"//列标题
      },
      {
        "name": "engine",
        "label": "Rendering engine"
      },
      {
        "name": "browser",
        "label": "Browser"
      },
      {
        "name": "platform",
        "label": "Platform(s)"
      },
      {
        "name": "version",
        "label": "Engine version"
      },
      {
        "name": "grade",
        "label": "CSS grade"
      }
    ]
  }
}

这样的schema渲染(render)出来的页面效果如下:

在这里插入图片描述
后端返回的数据片段如下:

{
	"status": 0,
	"msg": "ok",
	"data": {
		"count": 171,
		"rows": [{
			"engine": "Trident - 0jw14",
			"browser": "Internet Explorer 4.0",
			"platform": "Win 95+",
			"version": "4",
			"grade": "X",
			"badgeText": "默认",
			"id": 1
		}, {
			"engine": "Trident - acb3t",
			"browser": "Internet Explorer 5.0",
			"platform": "Win 95+",
			"version": "5",
			"grade": "C",
			"badgeText": "危险",
			"id": 2
		}, {
			"engine": "Trident - 0j7dqi",
			"browser": "Internet Explorer 5.5",
			"platform": "Win 95+",
			"version": "5.5",
			"grade": "A",
			"id": 3
		}, {
			"engine": "Trident - qvlpr",
			"browser": "Internet Explorer 6",
			"platform": "Win 98+",
			"version": "6",
			"grade": "A",
			"id": 4
		}, {
			"engine": "Trident - vdyua",
			"browser": "Internet Explorer 7",
			"platform": "Win XP SP2+",
			"version": "7",
			"grade": "A",
			"id": 5
		}, {
			"engine": "Trident - w4tz3",
			"browser": "AOL browser (AOL desktop)",
			"platform": "Win XP",
			"version": "6",
			"grade": "A",
			"id": 6
		}]
	}
}

上面那个最简单的schema根节点的body有一个api,如下:“api”: “/amis/api/mock2/sample”,这个api是body节点(也就是crud组件)获取数据的api,在组件渲染之后自动触发。

查询api

把这个最简单的schema去除注释(schema文件里面不支持注释)并修改api为"api": “/api/user/find”,然后copy覆盖掉public/json/index.json文件。保存后刷新浏览器,出现界面如下:
在这里插入图片描述
上图中api请求失败,主要是因为method错误,amis修改method比较容易,在api地址前面加上"post:",修改成这样:

"api": "post:/api/user/find", 

再次保存和刷新页面,可以看到请求成功了,界面如下:
在这里插入图片描述
现在接下来需要做的就是把columns里面的列信息改成和后端返回的数据一致,就可以把后端查询出来的数据显示出来了,再次修改schema代码如下:

{
  "type": "page",
  "body": {
    "type": "crud",
    "api": "post:/api/user/find",   
    "columns": [
      {
        "name": "id",
        "label": "ID"
      },
      {
        "name": "createdAt",
        "label": "创建时间"
      },
      {
        "name": "email",
        "label": "Email"
      },
      {
        "name": "nickname",
        "label": "昵称"
      },
      {
        "name": "age",
        "label": "年龄"
      }
    ]
  }
}

保存并刷新浏览器,呈现出如下的界面:
在这里插入图片描述
现在,我们成功的把后端查询出来的用户表里面的数据在amis的curd组件中呈现出来了。

本测试案例显示出来的用户信息,如果你的数据库里面没有,请用postman的自动运行功能,添加20-30条数据以方便测试

这个界面的呈现是对的,观察后端返回的数据有20多条,amis的crud自动显示前20条,并且自动分页,但是如果我们去点击右边那个第二页按钮,我们会发现返回的数据并没有变化,还是和原来一样,并且界面上面也没有显示第二页的数据内容。

分页

这个是有原因的,我们回到Sails后端的源代码,看看UserController.ts里面的find函数,我们发现它通过query2Sails函数获取前端提交的body里面的page作为当前查询的页码,把body里面的pageSize作为查询时每一页的记录数。
也就是说,我们如果要控制分页查询,我们还需要对api请求进行干涉,我们需要重新组织请求的body部分的内容。为此,我们需要了解curd组件是怎么提交请求的,提交的时候都有什么参数给到最终的request里面。

fetcher参数观察

前面我们知道amis组件里面,我们提供fetcher函数的实现给amis组件,当amis组件需要请求api的时候,它就会触发fetcher函数,在fetcher函数里面,我们通过return amisRequest(url, method, { …data, …config }); 代码把api请求的任务交给utils/request.ts里面的umi-request库来实现。
打开utils/request.ts,添加两个控制台输出,console.log(‘[url]:’, url); console.log(‘[data]:’, options);
通过这两行代码,我们可以观察curd组件提交过来的请求url和options都有一些什么,代码片段如下:

export async function amisRequest(url: string, method?: string, options?: { [key: string]: any }) {
  console.log('[url]:', url);
  console.log('[data]:', options);
  let newMethod = '';
  if (!method) newMethod = 'GET';
  else newMethod = method.toUpperCase();
  switch (newMethod) {
    case 'GET':
      return remoteRequest.get(url, options);
    case 'POST':
      return remoteRequest.post(url, options);
    case 'DELETE':
      return remoteRequest.delete(url, options);
    case 'PUT':
      return remoteRequest.put(url, options);
  }
  // return remoteRequest(url, {
  //   method: 'Delete',
  //   ...(options || {}),
  // });
}

保存后,进入浏览器调试界面观察,看到如下结果:
在这里插入图片描述
传递过来的url没有问题,但是data就比较混乱,这个地方应该是amis组件的fetcher函数没有做好,找到fetcher函数,看到它提交给amisRequest之前,把crud的data转换成string并合并到options,为了更好的调试我们修改一下fetcher,修改后如下:

fetcher: ({
          url, // 接口地址
          method, // 请求方法 get、post、put、delete
          data, // 请求数据
          responseType,
          config, // 其他配置
          headers, // 请求头
        }: any) => {
          // eslint-disable-next-line no-param-reassign
          config = config || {};
          config.withCredentials = true;
          // eslint-disable-next-line @typescript-eslint/no-unused-expressions
          responseType && (config.responseType = responseType);

          if (config.cancelExecutor) {
            request.CancelToken = config.cancelExecutor;
            //config.cancelToken = new (axios as any).CancelToken(config.cancelExecutor);
          }

          config.headers = headers || {};

          if (method !== 'post' && method !== 'put' && method !== 'patch') {
            if (data) {
              config.params = data;
            }
            //这个地方放弃合并,提交两个内容,一个是data,一个是config
            return amisRequest(url, method, data, config); // (axios as any)[method](url, config);
          } else if (data && data instanceof FormData) {
            config.headers = config.headers || {};
            config.headers['Content-Type'] = 'multipart/form-data';
          } else if (
            data &&
            typeof data !== 'string' &&
            !(data instanceof Blob) &&
            !(data instanceof ArrayBuffer)
          ) {
            // eslint-disable-next-line no-param-reassign
            //data = JSON.stringify(data); 屏蔽把data转换成string的代码
            config.headers = config.headers || {};
            config.headers['Content-Type'] = 'application/json';
          }
          //return (axios as any)[method](url, data, config);
          return amisRequest(url, method, data, config);
        },

因为data和config不再合并,amisRequest函数也应该做相应修改:

/*
 * amis的schema文件专用request
 */
export async function amisRequest(url: string, method?: string, data?: any, options?: { [key: string]: any }) {
  console.log('[url]:', url);
  console.log('[data]:', data);
  let newMethod = '';
  if (!method) newMethod = 'GET';
  else newMethod = method.toUpperCase();  

  switch (newMethod) {
    case 'GET':
      return remoteRequest.get(url, options);
    case 'POST':
      return remoteRequest.post(url, options);
    case 'DELETE':
      return remoteRequest.delete(url, options);
    case 'PUT':
      return remoteRequest.put(url, options);
  }
}

修改后,可以观察到提交的url和data数据了:
在这里插入图片描述

统一处理method

在amisRequest函数里面,我们是可以根据fetcher函数提交过来的url来重新组织后端请求的。这样schema里面的“api”:“post:…”,这种写法就可以简化一些了,因为method可以在amisRequest里面最终决定,并且我们的后端也主张尽量使用post,所以我们不要在amis的schema中指定api的method,因为最后面我们的schema可能要交给经过我们培训的非编程人员来修改,对他们来说能少理解一个专业术语就尽量少理解一个是最好的。综上,修改public/json/index.json中的api如下:

 "api": "/api/user/find",   

保存并刷新后观察到控制台输出是这样的:

[url]: /api/user/find?page=1&perPage=10
[data]: undefined

也就是说,crud对非post的api,默认会把查询参数page和perPage写到url里面,我们再次修改utils/request.ts,对传递过来的url和data重新处理,利用正则表达式把url里面的参数提取出来,代码如下:

/* eslint-disable */
import { extend, RequestOptionsInit } from 'umi-request';
import { getToken } from './myStorage';
//api 网址前缀,为了方便日后更换服务器,直接用一个常量定义
const urlPrefix = 'http://localhost:1898';
const remoteRequest = extend({
  // 路径前缀(基础路径)
  prefix: urlPrefix,
  timeout: 5000,
});
/**
 * 读取本地文件
 */
export const localRequest = extend({
  prefix: '',
  timeout: 5000,
});
//请求拦截器,拦截每个请求,添加完token之后再发送到后端
remoteRequest.interceptors.request.use((url: string, options: RequestOptionsInit) => {
  const headers = getToken()
    ? {
      authorization: `Bearer ${getToken()}`,
    }
    : { authorization: ' ' };

  return {
    url,
    options: { ...options, interceptors: true, headers: headers },
  };
});
/**
 * 用正则表达式捕获网址里面的分页信息
 * @param url
 */
function getPageInfoByReg(url: string): { page: number; pageSize: number } {
  let res = { page: 1, pageSize: 15 };
  let pageRegex = /page=(\d+)/;
  let perPageRegex = /perPage=(\d+)/;

  let pageMatch = url.match(pageRegex);
  let perPageMatch = url.match(perPageRegex);
  if (pageMatch && pageMatch.length > 1) res.page = parseInt(pageMatch[1]);
  if (perPageMatch && perPageMatch.length > 1) res.pageSize = parseInt(perPageMatch[1]);
  return res;
}
/**
 * 分解出Url里面的查询参数,并返回数据对象
 */
export function splitUrl(url: string): any {
  let urlDataList = url.split('?');
  let apiUrl = urlDataList[0];
  let searchData = {}, pageInfo = getPageInfoByReg(url);
  if (urlDataList.length > 1) {
    let paramsLst = urlDataList[1].split('&');
    for (var i = 0; i < paramsLst.length; i++) {
      var pair = paramsLst[i].split('=');
      if (pair.length < 2) continue;
      pair[1] = decodeURI(pair[1]);
      if (pair[0] == 'page' || pair[0] == 'perPage') continue;
      else searchData[pair[0]] = pair[1];
    }
  }
  searchData = {
    url: apiUrl,
    data: { ...searchData, page: pageInfo.page, pageSize: pageInfo.pageSize },
  };
  return searchData;
}
/*
 * amis的schema文件专用request
 */
export async function amisRequest(url: string, method?: string, data?: any, options?: { [key: string]: any }) {
  console.log('[url]:', url);
  console.log('[data]:', data);
  let newMethod = '';
  if (!method) newMethod = 'GET';
  else newMethod = method.toUpperCase();
  let sd = splitUrl(url);
  if (sd.url == '/api/user/find') {
    return remoteRequest(sd.url, {
      method: 'POST',
      data: sd.data,
      ...(options || {}),
    });
  }

  switch (newMethod) {
    case 'GET':
      return remoteRequest.get(url, options);
    case 'POST':
      return remoteRequest.post(url, options);
    case 'DELETE':
      return remoteRequest.delete(url, options);
    case 'PUT':
      return remoteRequest.put(url, options);
  }
}
export default remoteRequest;

分页参数提交到后端

crud提交过来的page和perPage分别代表当前页码和每一页记录数,现在我们通过splitUrl把这两个参数提取出来了,但是不能直接发送给后端,因为根据后端sails对分页的参数是要求名称分别page和pageSize,上述代码中,splitUrl函数已经把这个工作给做了,所以在提交的时候,提交sd.data里面就包含有page和pageSize了,请注意观察:

//splitUrl 函数返回的searchData对象把?后面的参数解析成data,把?前面的url返回到searchData.url
searchData = {
    url: apiUrl,
    data: { ...searchData, page: pageInfo.page, pageSize: pageInfo.pageSize },
  };
.....................
//amisRequest请求里面把解析后的sd.data提交到body里面:

if (sd.url == '/api/user/find') {
    return remoteRequest(url, {
      method: 'POST',
      data: sd.data,
      ...(options || {}),
    });
  }

以上request.ts代码修改后,发现分页按钮可以用了,点击不同的分页按钮,后端返回的数据和curd组件里面呈现的数据都符合要求了。

自定义分页和页面大小(pageSize)

目前我可以看到的是crud自动设置的分页pageSize大小是每页10条数据,应该有更个性化的设置,在schema里面columns前面添加如下内容:

"footerToolbar": [
      "statistics",//显示统计数据,比如1/3 总共:22 项
      "switch-per-page",//可以切换perPage值(设置pageSize)
      "pagination"//分页组件
    ],

保存后,界面就有可以设置分页大小的按钮了。如果需要强制设置分页大小,也可以指定api的默认参数,比如:

"defaultParams": {
      "perPage": 15
    }, 

搜索

单有数据展示是不够的,我们经常会需要根据关键词进行搜索过滤的功能,crud组件提供非常方便的写法:只需要在body里面添加一条"autoGenerateFilter": true 即可,代码片段如下:

"type": "crud",
 "api": "/api/user/find",
 "autoGenerateFilter": true,

如果现在保存并刷新,界面上看不到不一样,因为我们只是开启了过滤功能,并没有指定过滤字段,接下来我们尝试可以通过id搜索指定id值的记录,修改columns里面的id字段设置,增加一条"searchable": true, 代码片段如下:

{
        "name": "id",
        "searchable": true,
        "label": "ID"
      },

刷新保存后,可以看到如下界面:
在这里插入图片描述
id搜索的界面有了,我们尝试在输入框中输入3,点击搜索按钮,F12可以看到,这个时候提交过来的URL里面带有我们要搜索的id值了:
在这里插入图片描述
我们知道,Sails后端如果要搜索id=3,我们当时在postman里面是要在body里面设置{id:3} 才可以的,现在通过url里面的参数为什么也可以成功的返回id=3的数据呢,这个是因为我们前面在utils/request.ts里面添加的splitUrl函数,这个函数会把参数转换成data,然后写到post的body里面去。可以往前翻看一下splitUrl的代码。

id搜索目前看起来都不错,但是我们希望能有更友好一点的界面,比如通过placeholder提示用户怎么操作,我们可以修改searchable的值,从而做更具体的设置:

 "searchable": {
    "type": "input-text",
    "name": "id",
    "label": "主键",
    "placeholder": "输入id"
}

还可以添加更多的过滤字段,比如email:

 "searchable": {
          "type": "input-text",
          "name": "email",
          "label": "Email",
          "placeholder": "Email 模糊搜索"
        }

在这里插入图片描述

排序

对于呈现出来的数据,还会有排序的要求。crud依然提供非常方便的操作,想要通过哪个字段排序,只需要简单的添加一个 “sortable”: true,就可以了。比如我们希望可以按照id排序,可以按照createdAt字段排序,那么我们的columns里面可以这样改:

{
        "name": "id",
        "label": "ID",
        "sortable": true,//增加sortable
        "searchable": {
          "type": "input-text",
          "name": "id",
          "label": "主键",
          "placeholder": "输入id"
        }
      },
      {
        "name": "createdAt",
        "label": "创建时间",
        "sortable": true     //增加sortable   
      },

保存后界面会变成这样:
在这里插入图片描述
打开F12,点击ID排序按钮,观察传递过来的url和data:
在这里插入图片描述
可以看到url的参数里面增加了orderBy和orderDir,同时Sails后端返回一个500内部错误的信号,观察network 可以看到提交的body里面是这样的:
在这里插入图片描述
我看到前端把orderBy和orderDir提交到后端了,切换到Sails源代码,找到控制器的find函数,代码片段如下:

 let query = query2Sails('user', req._sails, req.body);

    if (query == false) {
      res.serverError("查询参数有误,请检查字段名称是否正确");
      return;
    }

这个地方,把前端的body拿去做转换,这个转换是为了完成前后端查询的约定。(相关博文在《二、 在Sails中使用Typescript》中有解释,需要回顾可以点击跳转过去)

query2Sails函数里面对排序字段的要求是options.sort = body.sort;为了防止查询参数有误,如果查询的参数不是数据库的字段或查询允许的其他关键词(比如page,pageSize,sort)该函数会返回false,告诉前端查询参数有问题,需要调整。

基于上述内容,前端需要对排序参数进行重新组织:用正则表达式把Url里面的orderBy和orderDir提取出来:

/**
 * 用正则表达式捕获网址里面的sort
 * @param url
 */
function getSortByReg(url: string): string {
  let res = '';
  let orderByRegex = /orderBy=(\w+)/;
  let orderDirRegex = /orderDir=(\w+)/;
  let orderByMatch = url.match(orderByRegex);
  let orderDirMatch = url.match(orderDirRegex);
  if (orderByMatch && orderByMatch.length > 1) res = orderByMatch[1];
  if (orderDirMatch && orderDirMatch.length > 1) res = `${res} ${orderDirMatch[1]}`;
  return res;
}

修改splitUrl函数如下:

/**
 * 分解出Url里面的查询参数,并返回数据对象
 */
export function splitUrl(url: string): any {
  let urlDataList = url.split('?');
  let apiUrl = urlDataList[0];
  let searchData = {},
    pageInfo = getPageInfoByReg(url),
    sort = getSortByReg(url);

  if (urlDataList.length > 1) {
    let paramsLst = urlDataList[1].split('&');
    for (var i = 0; i < paramsLst.length; i++) {
      var pair = paramsLst[i].split('=');
      if (pair.length < 2) continue;
      pair[1] = decodeURI(pair[1]);
      if (pair[0] == 'page' || pair[0] == 'perPage') continue;
      else if (pair[0] == 'orderBy' || pair[0] == 'orderDir') continue;
      else searchData[pair[0]] = pair[1];
    }
  }
  searchData = {
    url: apiUrl,
    data: { ...searchData, page: pageInfo.page, pageSize: pageInfo.pageSize, sort: sort },
  };
  return searchData;
}

保存,刷新。现在我们可以正常排序了,单击id或创建时间旁边的排序小按钮,观察输出,观察network里面,我们提交到find的body。(Request Payload)

有时候我们还会要求,crud一开始就按照某种要求排序,比如最新的数据排在最前面(order by id desc),这种情况很好出来,直接把排序参数写在api里面,这样渲染后的第一次请求就会按照参数里面写的排序,比如:

 "api": "/api/user/find?orderBy=id&&orderDir=desc",

保存刷新后,可以看到程序出来的数据已经按照id降序了。

头部工具条

列折叠按钮

关于查询,crud提供了更多的功能,可以通过头部工具条呈现。比如列显示折叠按钮,在columns前面添加如下代码:

"headerToolbar": [
      {
        "type": "columns-toggler",        
        "align":"right" //可以通过指定对齐方式控制按钮在左边还是右边
      }
    ],

保存并刷新,可以看到如下界面:
在这里插入图片描述

Bug踩坑记:amis 的列折叠按钮不能有"draggable": true 的选项,以下代码正常:
{
“type”: “columns-toggler”,
“align”:“right”
},
如果增加一个可拖动,变成
{
“type”: “columns-toggler”,
“draggable”: true,
“align”:“right”
},
就会出现 [mobx-state-tree] You are trying to read or write to an object that is no longer part of a state tree.

这个bug 已经提交到官方issues.

刷新和导出excel

"headerToolbar": [
      {
        "type": "reload",
        "icon": "fa fa-refresh"
      },
      "export-excel",
      {
        "type": "columns-toggler",        
        "align":"right"
      }
    ],

自定义内容

还可以添加自定义的内容,比如显示一些我们希望呈现给用户的其他信息,在headerToolbar里面添加如下代码试试:

{
    "type": "tpl",
    "tpl": "一共有 ${count} 行数据。",//${count} 可以获取api返回的data里面的count,还可以尝试${sql} 看看有什么效果
    "className": "v-middle"
},

tpl是amis提供的模板,可以参考:

https://aisuda.bce.baidu.com/amis/zh-CN/docs/concepts/template
https://aisuda.bce.baidu.com/amis/zh-CN/components/tpl

amis 还提供更多其他按钮,可以到amis官网了解更多

可以提供两种删除功能

单条删除

在columns数组最后面添加一个操作列,操作列里面添加删除功能,代码如下:

 "columns": [
 ......
	{//添加操作栏
    "type": "operation",
    "label": "操作",
    "buttons": [ //操作栏里面可以有许多操作按钮,所以这个地方buttons后面跟的是一个数组
     	{
        	"label": "删除",
	        "type": "button",
    	    "actionType": "ajax",
	        "level": "danger",
    	    "confirmText": "确认要删除?",
	        "api": {//点击按钮之后提交的api
    	      "url": "/api/userDel",
        	  "data": {
	            "id": "${id}"
    	      }
        	}
	  }
    ]
	}
]//columns结束

保存刷新后,可以看到删除按钮,但是点击删除按钮后发现Sails返回“不可以无条件删除的错误”,跟踪点击按钮之后提交的api请求,控制台输出如下:

[url]: /api/userDel
[data]: {id: 22}

这里我们看到id并没有如预期的写在url参数里面,这和我们schema源代码里面api的新写法有关系,这里,通过${id}可以获取当前操作按钮所在行的数据的id值。考虑后面我们在批量删除的时候还有更多的id变换,直接写在url参数里面直观性比较差,amis 的api同时提供两种写法,直接写在url里面的在参数要求比较直观的时候比较合适。如果需要对参数进行变换或是参数比较多的时候,可以通过“data”属性进行设置,并且设置在data里面的数据是通过提交到data参数而不是url后面加“?”来传递到fetcher的。所以我们上述的写法,url里面没有参数,data里面有{id:22}

如果我们希望把删除的条件id=22写在查询参数里面,可以这样:

 "api": "/api/userDel?id=${id}",

现在我们针对新的写法,utils/request.ts里面也应该增加这种情况下,对data数据的获取,这个修改比较简单:

增加一条
if (sd.url == '/api/userDel') sd.data = data;
同时对sd.url的判断里面增加一个||sd.url= ='/api/userDel'即可

修改后的amisRequest函数如下:

export async function amisRequest(url: string, method?: string, data?: any, options?: { [key: string]: any }) {
  console.log('[url]:', url);
  console.log('[data]:', data);
  let newMethod = '';
  if (!method) newMethod = 'GET';
  else newMethod = method.toUpperCase();
  let sd = splitUrl(url);
  if (sd.url == '/api/userDel') sd.data = data;
  if (sd.url == '/api/user/find'||sd.url=='/api/userDel') {
    return remoteRequest(sd.url, {
      method: 'POST',
      data: sd.data,
      ...(options || {}),
    });
  }

  switch (newMethod) {
    case 'GET':
      return remoteRequest.get(url, options);
    case 'POST':
      return remoteRequest.post(url, options);
    case 'DELETE':
      return remoteRequest.delete(url, options);
    case 'PUT':
      return remoteRequest.put(url, options);
  }
}

保存再执行删除,观察network里面的Payload。

点击删除按钮,数据被删除并且提示删除成功。同时我们看到界面里面的对应数据也没有了。这就说明我们点击删除按钮之后,crud组件会自动重载

批量删除

有时候我们希望可以一次性删除多条数据,crud提供批量操作bulkActions,我们可以把它添加在headerToolbar中,代码片段如下:

"headerToolbar": [
      "bulkActions",
      {
        "type": "reload",
        "icon": "fa fa-refresh"
      },
      .........
]

当然保存之后界面上面不会有变化,因为我们只是在headerToolbar里面添加了一个批量动作的占位符而已,我们还需要对批量动作进行具体定义,在headerToolbar 后面添加同级内容bulkActions:

"headerToolbar": [
....
],
"bulkActions": [
  {
    "label": "批量删除",
    "actionType": "ajax",
    "api": {
      "url": "/api/userDel",
      "data": {
        "id": "${CONCATENATE('in$(',ids,')')}"
      }
    },
    "confirmText": "确定要批量删除?"
 }
],

批量删除里面有个actionType,这个是动作类型,和button按钮类似。其中ajax标识这个动作是一个提交到后端的ajax操作(也就是发出api请求),api里面的内容是点击批量删除按钮之后发出的api,通过采用data的方式传递数据,“id”: "KaTeX parse error: Expected '}', got 'EOF' at end of input: …CONCATENATE('in(‘,ids,’)')}"这个语句利用amis提供的字符串连接公式,更多信息可以参考:amis表达式

因为我们要根据提供的id值删除多条数据,批量操作的 时候crud会提供当前选择的所有id值到ids变量,比如我们选择id值为18,19两个数据,ids的值就应该是18,19

我们的sails源代码里面,对删除多条id的要求是提交的body里面应该这样写:id: “in$(18,19)”,删除函数会根据前后端约定要求,转换成符合sails的形式:{id:in: [18,19] } 最终再翻译成对应的sql查询语句的where条件。

为此我们通过amis表达式:CONCATENATE,合成一个in$(18,19) 的参数给amisRequest,最终提交到后端我们可以成功删除多条数据了。如下图:

在这里插入图片描述
保存并刷新,观察network。

新增数据

在headerToolbar里面添加一个可以打开对话框( “actionType”: “dialog”)的“新增”按钮,

  {
        "label": "新增",
        "type": "button",
        "actionType": "dialog",//action类型是打开对话框
        "level": "primary",
        "className": "m-b-sm",
        "dialog": {//对话框节点
          "title": "添加用户",//对话框title
          "body": {
            "type": "form",//对话框body里面添加form表单节点
            "api": "/api/userCreate",//form提交是请求的api
            "body": [//form里面的项目
              {
                "type": "input-text",//文本输入框组件
                "name": "email",              
                "label": "Email"
              },
              {
                "type": "input-text",
                "name": "nickname",              
                "label": "昵称"
              },
              {
                "type": "native-number",
                "name": "age",
                "label": "年龄"
              },
              {
                "type": "hidden",//隐藏类型组件
                "name": "password",
                "value": "123456"
              }
            ]
          }
        }
      },

就这样,我们添加了一个可以新增用户数据的功能,点击新增按钮,可以弹出新增对话框,在对话框里面填写数据并提交后,后请求/api/userCreate这个后端api。

保存刷新:
在这里插入图片描述
界面已经满足预期,打开F12,任意添加一些数据后点击确定,观察控制台输出:

[url]: /api/userCreate
[data]: {password: '123456', email: '89898', nickname: '88', age: '88', __super: {}}

这个调用fetcher的url和data也满足预期,接下来在utls/request.ts里面添加对userCreate这个api的拦截处理,把data post到userCreate的body 就可以了:

/**
 * 需要拦截处理的url名单 要处理的api越来越多,用数组来判断
 */
const toBeProcessUrl = ['/api/user/find', '/api/userCreate', '/api/userDel', '/api/user/updateOne'];
/*
 * amis的schema文件专用request
 */
export async function amisRequest(url: string, method?: string, data?: any, options?: { [key: string]: any }) {
  console.log('[url]:', url);
  console.log('[data]:', data);
  let newMethod = '';
  if (!method) newMethod = 'GET';
  else newMethod = method.toUpperCase();
  let sd = splitUrl(url);
  if (sd.url == '/api/userDel') sd.data = data;
  else sd.data = { ...sd.data, ...data };
  if (toBeProcessUrl.includes(sd.url)) {//要处理的api越来越多,用数组来判断
    return remoteRequest(sd.url, {
      method: 'POST',
      data: sd.data,
      ...(options || {}),
    });
  }

  switch (newMethod) {
    case 'GET':
      return remoteRequest.get(url, options);
    case 'POST':
      return remoteRequest.post(url, options);
    case 'DELETE':
      return remoteRequest.delete(url, options);
    case 'PUT':
      return remoteRequest.put(url, options);
  }
}

保存刷新后,可以新增用户数据了。

headerToolbar 结果分析

一步一步过来,这个schema越来越复杂了,为了更加清晰的了解整个schema结构,我们把headerToolbar结构分析成如下图:
在这里插入图片描述

在编写schema 文档的时候,可以尽量的利用vscode的代码折叠功能,把复杂的当前不关心的节点折叠起来,这样有利于我们修改和管理schema源代码

前端数据格式要求

在测试过程,随意的添加了一个email,有时候根本不符合email的格式,但是依然可以顺利的提交并完成添加。这样的程序是不够严谨的,在前端,用户输入的时候,就应该对用户的输入做一些必要的格式检查。
amis 有提供格式校验的功能,详细资料可以参考:amis 格式校验

现在我们来实践一下对email的输入格式校验,在新增按钮的form里面,找到email输入项,修改如下:

{
	"type": "input-text",
	"name": "email",
	"required": true,//不允许空白
	"validations": {//校验参数
	"isEmail": true
	},
	"label": "Email"
},

修改后再随意提交,就会被拒绝:

在这里插入图片描述

数据修改

修改是需要针对某一条数据的,所以我们应该把修改按钮放置在操作栏里面,在操作栏中添加修改按钮,该按钮和新增按钮一样需要弹窗:

 "columns": [
 ................
  {
        "type": "operation",
        "label": "操作",
        "buttons": [
        ............
        {
            "label": "修改",           
            "type": "button",
            "level": "warning",//按钮颜色和删除按钮区分一下
            "actionType": "dialog",
            "dialog": {
              "title": "信息修改",
              "body": {
                "type": "form",
                "api": "/api/user/updateOne",
                "body": [
                  {
                    "type": "hidden",
                    "name": "id",
                    "value":"${id}" //修改需要提交当前记录的id,用隐藏字段实现
                  },
                  {
                    "type": "hidden",
                    "name": "version",
                    "value":"${version}" //修改需要提交当前记录的版本号,用隐藏字段实现
                  },
                  {
                    "type": "input-text",
                    "name": "createdAt",
                    "disabled": true,//创建时间应该是不可改的
                    "label": "创建时间"
                  },
                  {
                    "type": "input-text",
                    "name": "email",
                    "required": true,
                    "validations": {
                      "isEmail": true
                    },
                    "label": "Email"
                  },
                  {
                    "type": "input-text",
                    "required": true,
                    "name": "nickname",
                    "label": "昵称"
                  },
                  {
                    "type": "native-number",
                    "name": "age",
                    "label": "年龄"
                  }
                ]
              }
            }
          },
        ]
  }
]

保存后,界面已经满足预期,但是提交到api依然错误:
在这里插入图片描述

重组提交数据

我们还需要对提交到修改api的数据进行重新组织,打开sails源代码里面wlSimulate.ts文件,找到updateOne函数,可以看到注释里面有提到修改api的测试数据格式:

/**
* postmant 测试数据:
* 
{  
  "where":{"id":51},    
  "valuesToSet":{
     "email":"updateTest",
     "age":38
  },
  "oldVer":0
}
*/

根据这个格式要求,修改前端代码utils/request.ts里面的amisRequest如下:

/*
 * amis的schema文件专用request
 */
export async function amisRequest(url: string, method?: string, data?: any, options?: { [key: string]: any }) {
  console.log('[url]:', url);
  console.log('[data]:', data);
  let newMethod = '';
  if (!method) newMethod = 'GET';
  else newMethod = method.toUpperCase();
  let sd = splitUrl(url);
  if (sd.url == '/api/userDel') sd.data = data;
  else if (sd.url == '/api/user/updateOne') {//处理修改数据
    sd.data = {
      where: { id: data.id },
      valuesToSet: data,
      oldVer: data.version,
    };    
  } else sd.data = { ...sd.data, ...data };
  if (toBeProcessUrl.includes(sd.url)) {
    return remoteRequest(sd.url, {
      method: 'POST',
      data: sd.data,
      ...(options || {}),
    });
  }

  switch (newMethod) {
    case 'GET':
      return remoteRequest.get(url, options);
    case 'POST':
      return remoteRequest.post(url, options);
    case 'DELETE':
      return remoteRequest.delete(url, options);
    case 'PUT':
      return remoteRequest.put(url, options);
  }
}

保存修改并刷新,测试修改数据,显示修改成功。打开HeidiSQL查看被修改过的数据,可以看到版本号version已经自动增加,说明修改功能符合预期。

修改左边菜单

修改左边菜单数据,打开public/json/menu/zh_CN.json 修改第一条为:

{
        "path": "index",
        "name": "user表的增删改查"        
},

最终程序界面:

在这里插入图片描述

至此,对一个表的增删改查排序等操作都得以实现。熟悉schema比自己从头开始写代码做UI,amis的schema还是要高效非常多的。我们甚至连一行css代码都没有写,就搞定了一个高可用的增删改查页面了。

感谢amis团队!

转载请注明出处或者链接地址:https://www.qianduange.cn//article/12737.html
标签
评论
发布的文章
大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!