回顾
上一节我们以编写项目列表
为例子,讲解了一个相对完整的demo,其实只完成了查询和新增的功能,由于篇幅和时间的关系,这些笔者都会补全,但是可能不会完全讲解,所以大家可以对照代码查看对应的代码模块。
这一节我们开始设计项目的详情页面。
设计项目页面
在项目列表页面,我们只能看到项目的缩略,如果我们点进去项目的话,是需要能够看到这个项目的具体信息的。所以我们设计三个板块,以tab的形式展示:
- 用例树
- 成员列表
- 项目设置
设计用例路由
在antd pro里面支持参数路由,举个例子,我们针对不同的项目要展示不同的内容,这里就要用到参数路由了。举个例子,当项目id是1的时候,我的路由可能是/project/1
配置config/routes.js
image
我们创建了这样一个参数路由,并把hideInMenu
设置为true,也就是说不显示于左侧菜单栏。同时,这个路由对应的是ProjectDetail组件。
编写后端接口
我们目前只有一个查询项目列表的接口,但是我们现在是没有用例树的,所以暂时这个项目只获取到项目信息和项目角色。
- ProjectRoleDao.py中新增list_role方法
image
通过project_id去获取这个项目的所有角色列表。
- ProjectDao.py中新增query_project方法
image
先获取到项目详情,然后获取项目角色,这边的话笔者是没有用join或者子查询的,因为感觉sqlalchemy用起来不是很方便,大家也可以自由发挥。
注意,笔者会返回很多err或者None(因为可能受到了go写法的影响,这里大家可以自己按照自己的方式去写
)
- 编写/project/query接口
image
这部很简单,老规矩先挂上权限和路由的装饰器,接着对project_id进行参数检查,然后生成一个空的dict,把role和project信息查询出来以后写入result。
编写页面部分
- 先看下大致效果:
image
这边分了3个tab,第一个是用例列表,到时候会呈现一个用例树,左侧呢会根据用例的tag/用例的级别去展示该项目下的用例,右边呢则是用例的具体信息。
成员列表会显示这个项目下的成员,页面参考Yapi
。
项目设置可以让用户对项目的基础信息
进行一个更改,大概的页面功能模块是这样。
可以看到最终效果里面是没有具体的成员列表和项目设置的,我们先完成一个空壳,后续再进行补充。
编写ProjectDetail.jsx
import React, { useEffect, useState } from 'react'; import { PageContainer } from '@ant-design/pro-layout'; import { Avatar, Card, Tabs } from 'antd'; import { useParams } from 'umi'; import { process } from '@/utils/utils'; import { queryProject } from '@/services/project'; import auth from '@/utils/auth'; const { TabPane } = Tabs; export default () => { const params = useParams(); const projectId = params.id; const [projectData, setProjectData] = useState({}); const [roles, setRoles] = useState([]); const fetchData = async () => { const res = await queryProject({ projectId }); if (auth.response(res)) { setProjectData(res.data.project); setRoles(res.data.role); } }; useEffect(async () => { await process(fetchData); }, []); return ( <PageContainer title={<span> <Avatar style={{ backgroundColor: '#87d068' }}>{projectData.name === undefined ? 'loading...' : projectData.name.slice(0, 2)}</Avatar>{projectData.name}</span>}> <Card> <Tabs defaultActiveKey='1'> <TabPane tab='用例列表' key='1'> 这里没有用例,暂时替代一下 </TabPane> <TabPane tab='成员列表' key='2'> {/* <ProjectRole /> */} </TabPane> <TabPane tab='项目设置' key='3'> {/* <ProjectInfo data={projectData} /> */} </TabPane> </Tabs> </Card> </PageContainer> ); };
代码很简短,其中设置了projectData和roles2个字段(用来存放项目信息和角色列表),然后组件加载的时候会去请求一下查询项目
的接口,projectId我们可以通过useParams hook获取:
const params = useParams(); const projectId = params.id;
剩下的"html"部分很简单了,就是标准的PageContainer+卡片的组合,然后里面嵌入了3个tab。
完善编辑项目功能
可以看到上面有被注释掉的ProjectInfo组件,这个是我们用来修改项目信息的,我们这就来完善它!
编写后端接口
- ProjectDap.py添加update_project方法
@staticmethod def update_project(user, role, project_id, name, owner, private, description): try: data = Project.query.filter_by(id=project_id, deleted_at=None).first() if data is None: return "项目不存在" data.name = name # 如果修改人不是owner或者超管 if data.owner != owner and (role < pity.config.get("ADMIN") or user != data.owner): return "您没有权限修改项目负责人" data.owner = owner data.private = private data.description = description data.updated_at = datetime.now() data.update_user = user db.session.commit() except Exception as e: ProjectDao.log.error(f"编辑项目: {name}失败, {e}") return f"编辑项目: {name}失败, {e}" return None
这里值得注意的地方是,我们只有项目负责人和超级管理员可以编辑项目,所以一旦owner发生变更,则需要对权限做一个判断。最后就是记得更改更新时间
和更新人
。
- 编写/project/update接口
@pr.route("/update", methods=["POST"]) @permission() def update_project(user_info): try: user_id, role = user_info["id"], user_info["role"] data = request.get_json() if data.get("id") is None: return jsonify(dict(code=101, msg="项目id不能为空")) if not data.get("name") or not data.get("owner"): return jsonify(dict(code=101, msg="项目名称/项目负责人不能为空")) private = data.get("private", False) err = ProjectDao.update_project(user_id, role, data.get("id"), data.get("name"), data.get("owner"), private, data.get("description", "")) if err is not None: return jsonify(dict(code=110, msg=err)) return jsonify(dict(code=0, msg="操作成功")) except Exception as e: return jsonify(dict(code=111, msg=str(e)))
这边同样也先校验参数,然后调用update_project方法。
src/services/project.js编写更新项目的方法
image
编写ProjectInfo.jsx
import React, { useEffect, useState } from 'react'; import { Row, Col, Select, Tooltip } from 'antd'; import CustomForm from '@/components/PityForm/CustomForm'; import { listUsers } from '@/services/user'; import { updateProject } from '@/services/project'; import auth from '@/utils/auth'; const { Option } = Select; export default ({ data }) => { const [users, setUsers] = useState([]); const fetchUsers = async () => { const res = await listUsers(); setUsers(res); }; useEffect(async () => { await fetchUsers(); }, []); const onFinish = async (values) => { const project = { ...data, ...values, }; const res = await updateProject(project); auth.response(res, true); }; const opt = <Select placeholder='请选择项目组长'> { users.map(item => <Option key={item.value} value={item.id}><Tooltip title={item.email}>{item.name}</Tooltip></Option>) } </Select>; const fields = [ { name: 'name', label: '项目名称', required: true, message: '请输入项目名称', type: 'input', placeholder: '请输入项目名称', component: null, }, { name: 'owner', label: '项目负责人', required: true, component: opt, type: 'select', }, { name: 'description', label: '项目描述', required: false, message: '请输入项目描述', type: 'textarea', placeholder: '请输入项目描述', }, { name: 'private', label: '是否私有', required: true, message: '请选择项目是否私有', type: 'switch', valuePropName: 'checked', }, ]; return ( <Row gutter={8}> <Col span={24}> <CustomForm left={6} right={18} record={data} onFinish={onFinish} fields={fields} /> </Col> </Row> ); }
其实这里fields和之前创建项目的fields重复定义了,等于存放了2份,但是这里我图方便就没有抽出来,因为怕以后这里有什么变化(说白了就是懒,但是千万别和我一样,能封装的还是封装)
然后在组件加载的时候会获取所有用户(因为我们需要修改组员),但是我突然想到,角色列表也会获取组员身份,所以我们把user的获取放到最外层,也就是Project层,这里就不多展示了,详细可看源码。
CustomForm是自己封装的一套通用表单,里面也是解析fields然后展示表单:
import { Button, Col, Form, Row, Tooltip, Upload } from 'antd'; import React from 'react'; import ProjectAvatar from '@/components/Project/ProjectAvatar'; import { SaveOutlined } from '@ant-design/icons'; import getComponent from './index'; const {Item: FormItem} = Form; export default ({left, right, formName, record, onFinish, fields, dispatch}) => { const [form] = Form.useForm(); const layout = { labelCol: {span: left}, wrapperCol: {span: right}, } return ( <Form form={form} {...layout} name={formName} initialValues={record} onFinish={onFinish} > <Row> <Col span={6}/> <Col span={12} style={{textAlign: 'center'}}> <Tooltip title="点击可修改头像" placement="rightTop"> <Upload customRequest={async fileData => { await dispatch({ type: 'project/uploadFile', payload: { file: fileData.file, project_id: record.id, } }) }} fileList={[]}> <Row style={{textAlign: 'center', marginBottom: 16}}> <ProjectAvatar data={record}/> </Row> </Upload> </Tooltip> </Col> <Col span={6}/> </Row> { fields.map(item => <Row> <Col span={6}/> <Col span={12}> <FormItem label={item.label} colon={item.colon || true} rules={ [{required: item.required, message: item.message}] } name={item.name} valuePropName={item.valuePropName || 'value'} > {getComponent(item.type, item.placeholder, item.component)} </FormItem> </Col> <Col span={6}/> </Row>) } <Row> <Col span={6}/> <Col span={12} style={{textAlign: 'center'}}> <FormItem {...{ labelCol: {span: 0}, wrapperCol: {span: 24}, }}> <Button htmlType="submit" type="primary"><SaveOutlined/>修改</Button> </FormItem> </Col> <Col span={6}/> </Row> </Form> ) }
大致就是把fields里面的json数据取出,然后按照顺序解析成表单,最后留一个修改的按钮,执行保存操作。
看下效果吧
image
这里可以看到最上方的项目名称还没有进行更改,所以我们需要重新获取下项目数据。
image
要做的就是传入fetchData方法,并在修改后执行这个方法。
- 更新后
可以看到变成了QQ三国