回顾

上一节我们以编写项目列表为例子,讲解了一个相对完整的demo,其实只完成了查询新增的功能,由于篇幅和时间的关系,这些笔者都会补全,但是可能不会完全讲解,所以大家可以对照代码查看对应的代码模块。

这一节我们开始设计项目的详情页面。

设计项目页面

项目列表页面,我们只能看到项目的缩略,如果我们点进去项目的话,是需要能够看到这个项目的具体信息的。所以我们设计三个板块,以tab的形式展示:

  • 用例树
  • 成员列表
  • 项目设置

设计用例路由

在antd pro里面支持参数路由,举个例子,我们针对不同的项目要展示不同的内容,这里就要用到参数路由了。举个例子,当项目id是1的时候,我的路由可能是/project/1

配置config/routes.js

测试平台系列(23) 编写项目详情页面 - 图1

我们创建了这样一个参数路由,并把hideInMenu设置为true,也就是说不显示于左侧菜单栏。同时,这个路由对应的是ProjectDetail组件。

编写后端接口

我们目前只有一个查询项目列表的接口,但是我们现在是没有用例树的,所以暂时这个项目只获取到项目信息和项目角色。

  • ProjectRoleDao.py中新增list_role方法

测试平台系列(23) 编写项目详情页面 - 图2

通过projectid去获取这个项目的所有角色列表_。

  • ProjectDao.py中新增query_project方法

测试平台系列(23) 编写项目详情页面 - 图3

先获取到项目详情,然后获取项目角色,这边的话笔者是没有用join或者子查询的,因为感觉sqlalchemy用起来不是很方便,大家也可以自由发挥


注意,笔者会返回很多err或者None(因为可能受到了go写法的影响,这里大家可以自己按照自己的方式去写)

  • 编写/project/query接口

测试平台系列(23) 编写项目详情页面 - 图4

这部很简单,老规矩先挂上权限和路由的装饰器,接着对project_id进行参数检查,然后生成一个空的dict,把role和project信息查询出来以后写入result。

编写页面部分

  • 先看下大致效果:

测试平台系列(23) 编写项目详情页面 - 图5

这边分了3个tab,第一个是用例列表,到时候会呈现一个用例树,左侧呢会根据用例的tag/用例的级别去展示该项目下的用例,右边呢则是用例的具体信息。

成员列表会显示这个项目下的成员,页面参考Yapi

项目设置可以让用户对项目的基础信息进行一个更改,大概的页面功能模块是这样。

可以看到最终效果里面是没有具体的成员列表和项目设置的,我们先完成一个空壳,后续再进行补充。

编写ProjectDetail.jsx

  1. import React, { useEffect, useState } from 'react';
  2. import { PageContainer } from '@ant-design/pro-layout';
  3. import { Avatar, Card, Tabs } from 'antd';
  4. import { useParams } from 'umi';
  5. import { process } from '@/utils/utils';
  6. import { queryProject } from '@/services/project';
  7. import auth from '@/utils/auth';
  8. const { TabPane } = Tabs;
  9. export default () => {
  10. const params = useParams();
  11. const projectId = params.id;
  12. const [projectData, setProjectData] = useState({});
  13. const [roles, setRoles] = useState([]);
  14. const fetchData = async () => {
  15. const res = await queryProject({ projectId });
  16. if (auth.response(res)) {
  17. setProjectData(res.data.project);
  18. setRoles(res.data.role);
  19. }
  20. };
  21. useEffect(async () => {
  22. await process(fetchData);
  23. }, []);
  24. return (
  25. <PageContainer title={<span>
  26. <Avatar
  27. style={{ backgroundColor: '#87d068' }}>{projectData.name === undefined ? 'loading...' : projectData.name.slice(0, 2)}</Avatar>{projectData.name}</span>}>
  28. <Card>
  29. <Tabs defaultActiveKey='1'>
  30. <TabPane tab='用例列表' key='1'>
  31. 这里没有用例,暂时替代一下
  32. </TabPane>
  33. <TabPane tab='成员列表' key='2'>
  34. {/* <ProjectRole /> */}
  35. </TabPane>
  36. <TabPane tab='项目设置' key='3'>
  37. {/* <ProjectInfo data={projectData} /> */}
  38. </TabPane>
  39. </Tabs>
  40. </Card>
  41. </PageContainer>
  42. );
  43. };

代码很简短,其中设置了projectData和roles2个字段(用来存放项目信息和角色列表),然后组件加载的时候会去请求一下查询项目的接口,projectId我们可以通过useParams hook获取:

  1. const params = useParams();
  2. const projectId = params.id;

剩下的”html”部分很简单了,就是标准的PageContainer+卡片的组合,然后里面嵌入了3个tab。

完善编辑项目功能

可以看到上面有被注释掉的ProjectInfo组件,这个是我们用来修改项目信息的,我们这就来完善它!

编写后端接口

  • ProjectDap.py添加update_project方法
  1. @staticmethod
  2. def update_project(user, role, project_id, name, owner, private, description):
  3. try:
  4. data = Project.query.filter_by(id=project_id, deleted_at=None).first()
  5. if data is None:
  6. return "项目不存在"
  7. data.name = name
  8. # 如果修改人不是owner或者超管
  9. if data.owner != owner and (role < pity.config.get("ADMIN") or user != data.owner):
  10. return "您没有权限修改项目负责人"
  11. data.owner = owner
  12. data.private = private
  13. data.description = description
  14. data.updated_at = datetime.now()
  15. data.update_user = user
  16. db.session.commit()
  17. except Exception as e:
  18. ProjectDao.log.error(f"编辑项目: {name}失败, {e}")
  19. return f"编辑项目: {name}失败, {e}"
  20. return None

这里值得注意的地方是,我们只有项目负责人超级管理员可以编辑项目,所以一旦owner发生变更,则需要对权限做一个判断。最后就是记得更改更新时间更新人

  • 编写/project/update接口
  1. @pr.route("/update", methods=["POST"])
  2. @permission()
  3. def update_project(user_info):
  4. try:
  5. user_id, role = user_info["id"], user_info["role"]
  6. data = request.get_json()
  7. if data.get("id") is None:
  8. return jsonify(dict(code=101, msg="项目id不能为空"))
  9. if not data.get("name") or not data.get("owner"):
  10. return jsonify(dict(code=101, msg="项目名称/项目负责人不能为空"))
  11. private = data.get("private", False)
  12. err = ProjectDao.update_project(user_id, role, data.get("id"), data.get("name"), data.get("owner"), private,
  13. data.get("description", ""))
  14. if err is not None:
  15. return jsonify(dict(code=110, msg=err))
  16. return jsonify(dict(code=0, msg="操作成功"))
  17. except Exception as e:
  18. return jsonify(dict(code=111, msg=str(e)))

这边同样也先校验参数,然后调用update_project方法。

src/services/project.js编写更新项目的方法

测试平台系列(23) 编写项目详情页面 - 图6

编写ProjectInfo.jsx

  1. import React, { useEffect, useState } from 'react';
  2. import { Row, Col, Select, Tooltip } from 'antd';
  3. import CustomForm from '@/components/PityForm/CustomForm';
  4. import { listUsers } from '@/services/user';
  5. import { updateProject } from '@/services/project';
  6. import auth from '@/utils/auth';
  7. const { Option } = Select;
  8. export default ({ data }) => {
  9. const [users, setUsers] = useState([]);
  10. const fetchUsers = async () => {
  11. const res = await listUsers();
  12. setUsers(res);
  13. };
  14. useEffect(async () => {
  15. await fetchUsers();
  16. }, []);
  17. const onFinish = async (values) => {
  18. const project = {
  19. ...data,
  20. ...values,
  21. };
  22. const res = await updateProject(project);
  23. auth.response(res, true);
  24. };
  25. const opt = <Select placeholder='请选择项目组长'>
  26. {
  27. users.map(item => <Option key={item.value} value={item.id}><Tooltip
  28. title={item.email}>{item.name}</Tooltip></Option>)
  29. }
  30. </Select>;
  31. const fields = [
  32. {
  33. name: 'name',
  34. label: '项目名称',
  35. required: true,
  36. message: '请输入项目名称',
  37. type: 'input',
  38. placeholder: '请输入项目名称',
  39. component: null,
  40. },
  41. {
  42. name: 'owner',
  43. label: '项目负责人',
  44. required: true,
  45. component: opt,
  46. type: 'select',
  47. },
  48. {
  49. name: 'description',
  50. label: '项目描述',
  51. required: false,
  52. message: '请输入项目描述',
  53. type: 'textarea',
  54. placeholder: '请输入项目描述',
  55. },
  56. {
  57. name: 'private',
  58. label: '是否私有',
  59. required: true,
  60. message: '请选择项目是否私有',
  61. type: 'switch',
  62. valuePropName: 'checked',
  63. },
  64. ];
  65. return (
  66. <Row gutter={8}>
  67. <Col span={24}>
  68. <CustomForm left={6} right={18} record={data} onFinish={onFinish} fields={fields} />
  69. </Col>
  70. </Row>
  71. );
  72. }

其实这里fields和之前创建项目的fields重复定义了,等于存放了2份,但是这里我图方便就没有抽出来,因为怕以后这里有什么变化(说白了就是懒,但是千万别和我一样,能封装的还是封装)

然后在组件加载的时候会获取所有用户(因为我们需要修改组员),但是我突然想到,角色列表也会获取组员身份,所以我们把user的获取放到最外层,也就是Project层,这里就不多展示了,详细可看源码。

CustomForm是自己封装的一套通用表单,里面也是解析fields然后展示表单:

  1. import { Button, Col, Form, Row, Tooltip, Upload } from 'antd';
  2. import React from 'react';
  3. import ProjectAvatar from '@/components/Project/ProjectAvatar';
  4. import { SaveOutlined } from '@ant-design/icons';
  5. import getComponent from './index';
  6. const {Item: FormItem} = Form;
  7. export default ({left, right, formName, record, onFinish, fields, dispatch}) => {
  8. const [form] = Form.useForm();
  9. const layout = {
  10. labelCol: {span: left},
  11. wrapperCol: {span: right},
  12. }
  13. return (
  14. <Form
  15. form={form}
  16. {...layout}
  17. name={formName}
  18. initialValues={record}
  19. onFinish={onFinish}
  20. >
  21. <Row>
  22. <Col span={6}/>
  23. <Col span={12} style={{textAlign: 'center'}}>
  24. <Tooltip title="点击可修改头像" placement="rightTop">
  25. <Upload customRequest={async fileData => {
  26. await dispatch({
  27. type: 'project/uploadFile',
  28. payload: {
  29. file: fileData.file,
  30. project_id: record.id,
  31. }
  32. })
  33. }} fileList={[]}>
  34. <Row style={{textAlign: 'center', marginBottom: 16}}>
  35. <ProjectAvatar data={record}/>
  36. </Row>
  37. </Upload>
  38. </Tooltip>
  39. </Col>
  40. <Col span={6}/>
  41. </Row>
  42. {
  43. fields.map(item => <Row>
  44. <Col span={6}/>
  45. <Col span={12}>
  46. <FormItem label={item.label} colon={item.colon || true}
  47. rules={
  48. [{required: item.required, message: item.message}]
  49. } name={item.name} valuePropName={item.valuePropName || 'value'}
  50. >
  51. {getComponent(item.type, item.placeholder, item.component)}
  52. </FormItem>
  53. </Col>
  54. <Col span={6}/>
  55. </Row>)
  56. }
  57. <Row>
  58. <Col span={6}/>
  59. <Col span={12} style={{textAlign: 'center'}}>
  60. <FormItem {...{
  61. labelCol: {span: 0},
  62. wrapperCol: {span: 24},
  63. }}>
  64. <Button htmlType="submit" type="primary"><SaveOutlined/>修改</Button>
  65. </FormItem>
  66. </Col>
  67. <Col span={6}/>
  68. </Row>
  69. </Form>
  70. )
  71. }

大致就是把fields里面的json数据取出,然后按照顺序解析成表单,最后留一个修改的按钮,执行保存操作。

看下效果吧

测试平台系列(23) 编写项目详情页面 - 图7

这里可以看到最上方的项目名称还没有进行更改,所以我们需要重新获取下项目数据。

测试平台系列(23) 编写项目详情页面 - 图8

要做的就是传入fetchData方法,并在修改后执行这个方法。

  • 更新后
    测试平台系列(23) 编写项目详情页面 - 图9

今天的内容就到这里了,进度很慢,更新很慢。周末愉快,看RNG VS TES

项目前端地址

项目后端地址