闲聊

本来打算自己记录总结一下自己这几年的一个工作内容,也没想过去大力宣传啥的,因为总觉得文章数量有限,也怕随时就会断更。今天被饭哥的指点了一些,也想通了。也许之前断更都是没有太多动力吧,如果有人看的话,也会被动更新的。

自己一直都不是很自信吧,因为确实见识过很多大神级别的人物。比如大师兄饭哥露露哥等等。本来也觉得文章可能受众面是没有相关经验的读者,希望大佬们看到文章的错误可以及时指出呀,我将继续虚心学习(带头冲锋)~

饭哥的公众号: 测试开发干货 (分享测试开发的一切干货,搜索测试开发,第一条就是)

榜一大哥赫赫(一个始终在努力的人)的公众号: 测试开发水货 看名字也知道是分享成长经验

回顾

上一节已经贴出了项目列表页面的全部前端代码,由于笔者前端也没有深入学习,所以只能给大家大概讲解一下吧。还是按照之前的几个部分来说吧!

状态管理

测试平台系列(22) 编写项目的增删改查接口和页面(4) - 图1

设置了如下变量:

  • data
    存放当前项目数组

  • pagination
    项目分页

  • visible
    创建项目表单是否可见,默认是否

  • users
    由于我们需要选择项目管理员,所以需要一个userId => 用户信息的映射。当然这个接口我们暂时还没有实现。

方法编写

  1. const fetchData = async (current=pagination.current, size=pagination.size) => {
  2. await process(async ()=> {
  3. const res = await listProject({page: current, size });
  4. if (auth.response(res)) {
  5. setData(res.data)
  6. setPagination({...pagination, total: res.total})
  7. }
  8. });
  9. }

这个方法是获取项目列表的方法,current代表page,size代表分页大小,如果不传入则是pagination默认的值。

listProject其实就是包装的request方法,请求后端服务。

  1. useEffect(async () => {
  2. await fetchData();
  3. }, [])
  4. const onSearchProject = async projectName => {
  5. await process(async ()=> {
  6. const res = await listProject({page: 1, size: pagination.size, name: projectName});
  7. if (auth.response(res)) {
  8. setData(res.data)
  9. setPagination({...pagination, current: 1, total: res.total})
  10. }
  11. });
  12. }

useEffectreact新版本的特性,它支持2个参数,第一个是方法,第二个是变量数组,传入空数组的话,则每次这个组件开始渲染的时候会调用且只调用一次方法。

我们如果正式使用的话,是会有多套测试环境的。如果我们对项目做了环境的区分,那么就应该在切换环境的时候,获取不同环境的项目。

假设我们需要实现这种功能,那么我们先创建一个env的变量,然后改写useEffect:

  1. useEffect(async () => {
  2. await fetchData();
  3. }, [env])

这样的话,每当env发生变化,这个方法就会自动执行一次。需要说明的是,这里只是介绍一下useEffect的使用方式,因为我们这边暂时还没有扩展到多套环境,所以此处我们选择[]即可

  1. const onHandleCreate = async values => {
  2. const res = await insertProject(values);
  3. if (auth.response(res, true)) {
  4. // 创建成功后自动获取第一页的数据, 因为项目会按创建时间排序
  5. await fetchData(1);
  6. }
  7. }

这边也如出一辙,创建完毕了之后如果接口未返回错误,则刷新项目列表页面,并且自动去第一页

  1. const content = (item) => {
  2. return <div>
  3. {/* <p>负责人: {userMap[item.owner].name}</p> */}
  4. <p>简介: {item.description || '无'}</p>
  5. <p>更新时间: {item.updated_at}</p>
  6. </div>
  7. };
  8. const opt = <Select placeholder="请选择项目负责人">
  9. {
  10. Object.keys(users).map(id => <Option key={id} value={id}>{users[id].name}</Option>)
  11. }
  12. </Select>
  13. const fields = [
  14. {
  15. name: 'projectName',
  16. label: '项目名称',
  17. required: true,
  18. message: "请输入项目名称",
  19. type: 'input',
  20. placeholder: "请输入项目名称",
  21. },
  22. {
  23. name: 'owner',
  24. label: '项目负责人',
  25. required: true,
  26. component: opt,
  27. type: 'select',
  28. },
  29. {
  30. name: 'description',
  31. label: '项目描述',
  32. required: false,
  33. message: "请输入项目描述",
  34. type: 'textarea',
  35. placeholder: "请输入项目描述",
  36. },
  37. {
  38. name: 'private',
  39. label: '是否私有',
  40. required: true,
  41. message: "请选择项目是否私有",
  42. type: 'switch',
  43. valuePropName: "checked",
  44. },
  45. ]

这里content是一个方法,会返回一个div的html结构,目的是为了展示项目的详情,比如负责人项目描述等。

fields的话,是表单的字段,因为我针对antd的form进行了一点封装,编写了一套高阶组件。等于说是规划好了表单里面的表单组成,由input和select以及switch组件组成

待会会讲这个组件!

最后看return里面的组件

  1. return (
  2. <PageContainer title={false}>
  3. <FormForModal width={600} title="添加项目" left={6} right={18} record={{}}
  4. visible={visible} onCancel={() => setVisible(false)} fields={fields} onFinish={onHandleCreate}
  5. />
  6. <Row gutter={8} style={{marginBottom: 16}}>
  7. <Col span={18}>
  8. <Button type="primary" onClick={() => setVisible(true)}>创建项目
  9. <Tooltip title="只有超级管理员可以创建项目"><QuestionCircleOutlined/></Tooltip>
  10. </Button>
  11. </Col>
  12. <Col span={6}>
  13. <Search onSearch={onSearchProject} style={{float: 'right'}} placeholder="请输入项目名称"/>
  14. </Col>
  15. </Row>
  16. <Spin spinning={false}>
  17. <Row gutter={16}>
  18. {
  19. data.length === 0 ? <Col span={24} style={{textAlign: 'center', marginBottom: 12}}>
  20. <Card><Empty description="暂无项目, 快点击『创建项目』创建一个吧!"/></Card>
  21. </Col> :
  22. data.map(item =>
  23. <Col key={item.id} span={4} style={{marginBottom: 12}}>
  24. <Popover content={content(item)} placement="rightTop">
  25. <Card hoverable bordered={false} style={{borderRadius: 16, textAlign: 'center'}}
  26. bodyStyle={{padding: 16}} onClick={() => {
  27. history.push(`/project/${item.id}`);
  28. }}>
  29. <Avatar style={{backgroundColor: '#87d068'}} size={64}
  30. >{item.name.slice(0, 2)}</Avatar>
  31. <p style={{
  32. textAlign: 'center',
  33. fontWeight: 'bold',
  34. fontSize: 18,
  35. marginTop: 8
  36. }}>{item.name}</p>
  37. </Card>
  38. </Popover>
  39. </Col>
  40. )
  41. }
  42. </Row>
  43. </Spin>
  44. </PageContainer>
  45. )
  46. }

PageContainer是最外层,也就是咱们看到的这一块:

测试平台系列(22) 编写项目的增删改查接口和页面(4) - 图2

FormForModal是一个对话框表单,默认是不显示的,只有点击创建项目才会显示。

然后用Row分了2行: 分别是 创建栏/搜索栏和项目展示栏

测试平台系列(22) 编写项目的增删改查接口和页面(4) - 图3

这边项目展示栏如果data.length === 0就展示一个空状态,提示用户去添加项目,否则就把项目展示出来,每个项目占屏幕的1/6,因为Col总共有24份。

tips: 这里类似于bootstrap,一行共有24份,所以span={4}代表的是4/24,可参考: 官网介绍

测试平台系列(22) 编写项目的增删改查接口和页面(4) - 图4

其他的就是Card(卡片)组件和Avatar(头像)组件的互相嵌入,Popover是悬浮窗口,如图:

测试平台系列(22) 编写项目的增删改查接口和页面(4) - 图5

编写获取用户列表的接口

修改app/dao/auth/UserDao.py

测试平台系列(22) 编写项目的增删改查接口和页面(4) - 图6

筛选出未被删除的用户即可。

修改app/controllers/auth/user.py

测试平台系列(22) 编写项目的增删改查接口和页面(4) - 图7

这里注意开启一下权限,但是不能控制太死,能让登录用户访问即可。

前端页面编写listUsers方法

测试平台系列(22) 编写项目的增删改查接口和页面(4) - 图8

看下效果

测试平台系列(22) 编写项目的增删改查接口和页面(4) - 图9

发现一个问题: 创建成功后,对话框没有关闭

所以我们需要在创建成果后,setVisible(false)去关闭对话框。

测试平台系列(22) 编写项目的增删改查接口和页面(4) - 图10

还有一个地方就是之前注释掉的项目负责人,现在可以重新开启了。

测试平台系列(22) 编写项目的增删改查接口和页面(4) - 图11

测试平台系列(22) 编写项目的增删改查接口和页面(4) - 图12

看下搜索效果

测试平台系列(22) 编写项目的增删改查接口和页面(4) - 图13

测试平台系列(22) 编写项目的增删改查接口和页面(4) - 图14

这就是本节的内容了,大部分是讲解代码为主,因为前端内容居多,所以可能有点懵逼。如果有不理解的地方,还请多多看看reactes6相关的教程。箭头函数,看着别怕,熟悉了就好。

下一节可能是具体项目的编辑页面了,感觉这一讲就讲了一个世纪,再后面就是用例那块了。