文章目录
- 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团队!