diff --git a/src/pages/Suite/components/Case/index.tsx b/src/pages/Suite/components/Case/index.tsx index 5ebf5ceb0491c679c9efd90da8e5211b58178268..b27d61f757c2257f9424f15983714f53c27e1b78 100644 --- a/src/pages/Suite/components/Case/index.tsx +++ b/src/pages/Suite/components/Case/index.tsx @@ -1,238 +1,351 @@ import React from "react" -import { Space, Row, Typography, Dropdown, Menu, Button, FormInstance, Input, message } from "antd" -import { MoreOutlined, DownOutlined, EditTwoTone } from "@ant-design/icons" +import { Button, Divider, Empty, Input, Row, Space, Typography, message } from "antd" +import { DownOutlined, UpOutlined, RightOutlined } from "@ant-design/icons" +// import SuiteList from "./SuiteList" +import BatchMoveCase from "./BatchMoveCase" +import BatchDeleteCase from "./BatchDeleteCase" +// import FilterForm from "./FilterForm" +import AddModal from "../AddModal" +import ExportCase from "./ExportCase" -import BaseChild from "./Base" -import EditChild from "./Edit" -import { renameCase, deleteCases, updateCase } from "@/pages/Suite/services" +import { exportCases, queryModalCases } from "../../services" +import { useParams, useAccess } from "umi" -import UserAndPriority from "@/components/Public/UserAndPriority" -import DeleteModal from "@/pages/Outline/components/DeleteModal" -import { priorityListOptions } from "../../utils" +import CaseChild from "@/pages/Suite/components/Case" +// import { useCaseProvider } from "../../provider" +import Loading from "@/components/Loading" +import SplitPane from "react-split-pane" import styled from "styled-components" -import { useAccess } from "umi" -import { PriorityTag } from "@/components/Public/PriorityTag" +import { useSize } from "ahooks" +import SuiteTable from "./SuiteTable" -type IProps = { - [k: string]: any -} - -const HoverMenu = styled(Menu)` - li:hover { - color: rgba(24,144,255,1); - background-color: #e6f7ff; - } -` +import CaseFilter from "../FilterForm/CaseFilter" +import BatchEditCase from "./BatchEditCase" +import moment from "moment" -const priorityText = new Map([ - [0, "系统不可缺少"], - [1, "重要而不必需"], - [2, "相对非核心功能"], - [3, "系统可靠性等功能"] -]) +const dateFormat = 'YYYY-MM-DD'; -const Case: React.FC = (props) => { - const access = useAccess() +const RightContainer = styled(Row)` + background-color: #ffffff; + height: calc(100% - 48px - 48px); - const { currentCase, refresh, height } = props + .SplitPane { position: unset!important;} - const [editing, setEditing] = React.useState(false) - const [rname, setRname] = React.useState(false) - const [inp, setInp] = React.useState("") + .Resizer { + background: #000; + opacity: 0.06; + z-index: 1; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + -moz-background-clip: padding; + -webkit-background-clip: padding; + background-clip: padding-box; + } - const [fetching, setFetching] = React.useState(false) + .Resizer:hover { + -webkit-transition: all 2s ease; + transition: all 2s ease; + } - const editInfoRef = React.useRef(null) as any - const deleteModalRef = React.useRef(null) as any + .Resizer.horizontal { + height: 11px; + margin: -5px 0; + border-top: 5px solid rgba(255, 255, 255, 0.06); + border-bottom: 5px solid rgba(255, 255, 255, 0.06); + cursor: row-resize; + width: 100%; + } - const handleMenuClick = (e: any) => { - switch (e.key) { - case "edit": return setEditing(true) - case "rename": return handleOpenRename() - case "delete": return deleteModalRef.current?.show() - } + .Resizer.horizontal:hover { + border-top: 5px solid rgba(0, 0, 0, 0.06); + border-bottom: 5px solid rgba(0, 0, 0, 0.06); } - const handleOpenRename = () => { - setRname(true) - setInp(currentCase?.name) + .Resizer.vertical { + width: 11px; + margin: 0 -5px; + border-left: 5px solid rgba(255, 255, 255, 0.06); + border-right: 5px solid rgba(255, 255, 255, 0.06); + cursor: col-resize; } - const handleDelete = async () => { - const { code, msg } = await deleteCases({ id: [currentCase.id] }) - if (code !== 200) return message.error(msg) - refresh() - message.success("操作成功!") + .Resizer.vertical:hover { + border-left: 5px solid rgba(0, 0, 0, 0.06); + border-right: 5px solid rgba(0, 0, 0, 0.06); + } + + .Resizer.disabled { + cursor: not-allowed; } - const handleSave = () => { - const infoForm: FormInstance & any = editInfoRef.current - infoForm.validateFields() - .then(async (values: any) => { - const params = infoForm?.formResultChangeValue() - if (!params) return - const { code, msg } = await updateCase(currentCase.id, params) - if (code !== 200) { - message.error(msg) - return - } - refresh() - message.success("操作成功!") - handleCancelSave() - }) + .Resizer.disabled:hover { + border-color: transparent; } +` + +type IProps = { + [k: string]: any +} + +const RightContent: React.FC = (props) => { + const access = useAccess() + + const { mod_id } = useParams() as any + const defaultParams = { mod_id } + + const [loading, setLoading] = React.useState(true) + const [cases, setCases] = React.useState([]) + const [selectCases, setSelectCases] = React.useState([]) + const [activeCase, setActiveCase] = React.useState(null) + + const [source, setSource] = React.useState(undefined) - const updatePriority = async (level: number) => { - if (fetching) return - setFetching(true) - const { code, msg } = await updateCase(currentCase.id, { ...currentCase, priority: level }) - setFetching(false) + const [filter, setFilter] = React.useState(false) + + const batchMoveRef = React.useRef(null) as any + const batchDeleteRef = React.useRef(null) as any + const createCaseRef = React.useRef(null) as any + const exportCaseRef = React.useRef(null) as any + const batchEditRef = React.useRef(null) as any + + const [pageParams, setPageParams] = React.useState(defaultParams) + + const getModalCase = async (params: any = pageParams, showLoading: boolean = true) => { + showLoading && setLoading(true) + const response = await queryModalCases(params) + setLoading(false) + const { code, data } = response if (code !== 200) { - message.error(msg) return } - refresh() - message.success("操作成功!") + setCases(data) + setSource(response) } - const handleRenameOk = async () => { - if (inp === currentCase?.name) { - setRname(false) - return + React.useEffect(() => { + if (cases.length > 0) { + if (activeCase && typeof activeCase.id === "number") { + const idx = cases.findIndex((i: any) => i.id === activeCase.id) + if (~idx) + setActiveCase(cases[idx]) + else setActiveCase(cases[0]) + } else + setActiveCase(cases[0]) } - const { code, msg } = await renameCase(currentCase.id, { name: inp }) - if (code !== 200) return message.error(msg) - setRname(false) - refresh() + }, [cases, activeCase]) + + React.useEffect(() => { + getModalCase({ mod_id }) + return () => { + setCases([]) + setLoading(true) + setSelectCases([]) + setFilter(false) + setActiveCase(null) + } + }, [mod_id]) + + const refreshCases = () => { + setSelectCases([]) + getModalCase({ ...pageParams, mod_id }) } - const handleCancelSave = () => { - setEditing(false) + const handleOkFilter = (vals: any) => { + const params = { mod_id, ...vals } + setPageParams(params) + getModalCase(params) + } + + const exportExcel = async () => { + if (!selectCases.length) return message.warning("请选择需要导出的用例!") + const data = await exportCases(selectCases) + const fileAppType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + const blob = new Blob([data], { + type: fileAppType, + }); + const objectURL = URL.createObjectURL(blob); + const btn = document.createElement('a'); + btn.download = `${Date.parse(new Date().toString())}.xlsx`; + btn.href = objectURL; + btn.click(); + URL.revokeObjectURL(objectURL); + message.success('下载成功'); + } + + const contentRef = React.useRef(null) as any + const contentSize = useSize(contentRef) + + const currentPath = React.useMemo(() => { + if (!source) return [] + const { path } = source + const list = (path as any).split("/").filter(Boolean) + return list + }, [source]) + + const handleFilterChange = (val: any) => { + const params = Object.keys(val).reduce((pre: any, cur: any) => { + const ctx = val[cur] + if (cur === "created_at") { + const [start_time, end_time] = ctx + pre["start_time"] = moment(start_time).format(dateFormat) + pre["end_time"] = moment(end_time).format(dateFormat) + } + pre[cur] = ctx + return pre + }, {}) + + getModalCase({ mod_id, ...params }, false) } - //width: 513, borderLeft: "1px solid rgba(0, 0, 0, 0.06)" return ( -
- - {currentCase?.gmt_created} - +
+
+ + + { + currentPath.length > 0 ? + currentPath.map((i: string, idx: any) => ( + + + {i} + + { + currentPath.length - 1 > idx && + + } + + )) : + 所有用例 + } + + ({cases.length}) + + { - editing && + access.canTester() && - - + + + } - { - access.canCurrentTester(currentCase?.creator) && - - - 编辑 - - - 重命名 - - - 删除 - - - } - > - - - - - } - - - - - { - rname ? - setInp(target.value)} - onBlur={handleRenameOk} - /> : - - {currentCase?.name} - handleOpenRename()} /> - - } - - 描述:{currentCase?.desc} - + - - { - priorityListOptions.map((i: any) => ( - updatePriority(i)} - style={ - currentCase?.priority === i ? - { - background: "#E6F7FF", - color: "#1890FF" - } : - {} - } - > - {`P${i} - ${priorityText.get(i)}`} - - )) - } - - } + + + { + access.canTester() && + <> + + + + + } + + + + { + handleOkFilter({ key: val }) + }} + // onKeyUp={handleKeyup} + /> + setFilter(!filter)}> + + + 筛选 + { + filter ? + : + + } + + + + + + + { + filter && + <> + + + + // + } + + + + { + cases.length === 0 ? + : + + {/* @ts-ignore */} + + + + { + getModalCase(defaultParams) + }} + /> + + + } +
+ { - editing ? - : - + loading && + } - + + + + + +
) } -export default Case \ No newline at end of file +export default RightContent \ No newline at end of file diff --git a/src/pages/Suite/components/RightContent/SuiteTable.tsx b/src/pages/Suite/components/RightContent/SuiteTable.tsx index 1e987b9e22d102139a33ca0cdc4f490182372ea5..f08f79acb3915e1679e92eb4e273079c2605852e 100644 --- a/src/pages/Suite/components/RightContent/SuiteTable.tsx +++ b/src/pages/Suite/components/RightContent/SuiteTable.tsx @@ -96,4 +96,4 @@ const SuiteTable: React.FC = (props) => { ) } -export default SuiteTable \ No newline at end of file +export default React.memo(SuiteTable) \ No newline at end of file diff --git a/src/pages/Suite/components/RightContent/index.tsx b/src/pages/Suite/components/RightContent/index.tsx index e809846644480517baca9242650f743edf090226..acf2fe1c8a07a5473eb1d9910088556368820c48 100644 --- a/src/pages/Suite/components/RightContent/index.tsx +++ b/src/pages/Suite/components/RightContent/index.tsx @@ -143,7 +143,6 @@ const RightContent: React.FC = (props) => { return () => { setCases([]) setLoading(true) - setInp("") setSelectCases([]) setFilter(false) setActiveCase(null) @@ -208,19 +207,52 @@ const RightContent: React.FC = (props) => { const wrapperHeight = wrapperSzie?.height ? wrapperSzie.height : 0 const caseContentHeight = wrapperHeight - 48 * 2 - filterHeight - const caseDomHeight = wrapperHeight - filterHeight - - return ( -
- - - { - currentPath.length > 0 ? - currentPath.map((i: string, idx: any) => ( - - - {i} - + + + { + access.canTester() && + <> + + + + + } + + + + { + handleOkFilter({ key: val }) + }} + // onKeyUp={handleKeyup} + /> + setFilter(!filter)}> + + + 筛选 { currentPath.length - 1 > idx && @@ -245,103 +277,36 @@ const RightContent: React.FC = (props) => { - - - - { - access.canTester() && - <> - - - - - } - - - - setInp(target.value)} - onSearch={() => handleOkFilter({ key: inp })} - // onKeyUp={handleKeyup} - /> - setFilter(!filter)}> - - - 筛选 - { - filter ? - : - - } - - - - - - { - filter && - <> - - - - } - - { - cases.length === 0 ? - : - - - - - { - getModalCase(defaultParams) - }} - /> - - - } + + + { + getModalCase(defaultParams) + }} + /> + + + } +
{ loading && diff --git a/src/pages/Suite/services.ts b/src/pages/Suite/services.ts index 415d72220cdc770ed5bf26182dacf8c284a6ed4b..45a26d13cab6ab0703d86e5441fa7a585c0cecd5 100644 --- a/src/pages/Suite/services.ts +++ b/src/pages/Suite/services.ts @@ -1,115 +1,116 @@ -import { request } from "umi" +import { request } from 'umi'; export type ICase = { name?: string; - run_method?: "manual" | "auto"; - run_model?: "single" | "cluster"; + run_method?: 'manual' | 'auto'; + run_model?: 'single' | 'cluster'; is_available?: boolean; custom_fields?: any; tone_case?: any; custom_field?: any; - parent?: any + parent?: any; priority?: number; -} +}; //新增测试用例 export const createCases = async (data: ICase) => { - return request(`/api/case/create/`, { method: "post", data }) -} + return request(`/api/case/create/`, { method: 'post', data }); +}; export type IModal = { mod_id: any; page_num?: any; page_size?: any; -} +}; //查个每个模块下的测试用例 export const queryModalCases = async (params: IModal) => { - return request(`/api/case/search/`, { method: "get", params }) -} + return request(`/api/case/search/`, { method: 'get', params }); +}; export const queryModalTree = async (parent: any) => { - return request(`/api/case/module/tree/${parent}`) -} + return request(`/api/case/module/tree/${parent}`); +}; //重命名测试用例 export const renameCase = async (case_id: any, data: { name: string }) => { - return request(`/api/case/rename/${case_id}`, { method: "post", data }) -} + return request(`/api/case/rename/${case_id}`, { method: 'post', data }); +}; //批量删除测试用例 export const deleteCases = async (params: { id: any }) => { - return request(`/api/case/`, { method: "delete", params }) -} + return request(`/api/case/`, { method: 'delete', params }); +}; //移动测试用例 -export const moveCase = async (data: { parent: any, cases: any[] }) => { - return request(`/api/case/move`, { method: "post", data }) -} +export const moveCase = async (data: { parent: any; cases: any[] }) => { + return request(`/api/case/move`, { method: 'post', data }); +}; //更新测试用例 export const updateCase = async (case_id: string, data: ICase) => { - return request(`/api/case/edit/${case_id}`, { method: "post", data }) -} + return request(`/api/case/edit/${case_id}`, { method: 'post', data }); +}; export type IModule = { name: string; level?: any; parent?: any; -} +}; //创建分类模块 export const createModule = async (data: IModule) => { - return request(`/api/case/module/create`, { method: "post", data }) -} + return request(`/api/case/module/create`, { method: 'post', data }); +}; //获得某一模块写的所有子模块 export const queryModules = async (node_id: any, params?: any) => { - return request(`/api/case/module/${node_id}`, { params }) -} + return request(`/api/case/module/${node_id}`, { params }); +}; //根据前缀获得相同前缀的所有模块名称 export const queryModuleName = async (params: { start?: string }) => { - return request(`/api/case/modules`, { params }) -} + return request(`/api/case/modules`, { params }); +}; //重命名模块名称 export const renameModule = async (mod_id: any, data: { name: string }) => { - return request(`/api/case/module/rename/${mod_id}`, { method: "post", data }) -} + return request(`/api/case/module/rename/${mod_id}`, { method: 'post', data }); +}; //移动模块路径 -export const moveMudal = async (mod_id: any, data: { level: any, parent: any }) => { - return request(`/api/case/module/move/${mod_id}`, { method: "post", data }) -} +export const moveMudal = async (mod_id: any, data: { level: any; parent: any }) => { + return request(`/api/case/module/move/${mod_id}`, { method: 'post', data }); +}; //删除模块(只删除模块,模块用例移动到删除模块的父模块下) export const deleteModule = async (mod_id: string) => { - return request(`/api/case/module/${mod_id}`, { method: "delete" }) -} + return request(`/api/case/module/${mod_id}`, { method: 'delete' }); +}; //从excel导入测试用例(只支持.xls,.xlsx) export const importExcelCase = async (data: { excel: any }) => { - return request(`/api/case/import/`, { method: "post", data }) -} + return request(`/api/case/import/`, { method: 'post', data }); +}; type CaseSearchQuery = { - key: string; //查找的关键词 - page_size?: number;//页面大小,默认1 - page_num?: number;// 分页页数,默认50 - type?: string;//测试用例类型,功能测试,性能测试,默认全部 - run_method?: string;// 用例执行方式,默认全选 - run_model?: string;// 用例运行模式,默认全选 - is_available?: boolean;// 用例是否可用,默认全选 - device_type?: string;// 用例执行机器类型,默认全选 - creator?: string;// 创建人,默认不区分 - start_time?: string;// 开始时间 - end_time?: string;// 结束时间 + key?: string; //查找的关键词 + id?: string; + page_size?: number; //页面大小,默认1 + page_num?: number; // 分页页数,默认50 + type?: string; //测试用例类型,功能测试,性能测试,默认全部 + run_method?: string; // 用例执行方式,默认全选 + run_model?: string; // 用例运行模式,默认全选 + is_available?: boolean; // 用例是否可用,默认全选 + device_type?: string; // 用例执行机器类型,默认全选 + creator?: string; // 创建人,默认不区分 + start_time?: string; // 开始时间 + end_time?: string; // 结束时间 mod_id?: string; -} +}; //查找用例 export const queryCases = async (params: CaseSearchQuery) => { - return request(`/api/case/`, { params }) -} + return request(`/api/case/`, { params }); +}; //获取workspace用例 /* @@ -123,11 +124,11 @@ type ToneSuiteParams = { domain?: string; test_type?: string; key?: string; -} +}; export const queryToneSuite = async (params: ToneSuiteParams) => { - return request(`/api/tone/suites`, { params }) -} + return request(`/api/tone/suites`, { params }); +}; /* ● 必选参数: @@ -139,27 +140,30 @@ export const queryToneSuite = async (params: ToneSuiteParams) => { type ToneSuiteCaseParams = { suite_id?: number; -} +}; export const queryToneSuiteCase = async (suite_name: string, params?: ToneSuiteCaseParams) => { - return request(`/api/tone/${suite_name}/cases`, { params }) -} + return request(`/api/tone/${suite_name}/cases`, { params }); +}; //获取领域 export const queryDomainList = async () => { - return request(`/tone/api/case/test_domain/?test_type=domainconf`) -} + return request(`/tone/api/case/test_domain/?test_type=domainconf`); +}; export const downloadCaseTempFile = async () => { - return request(`/api/case/export`, { method: 'get', responseType: 'blob' }) -} + return request(`/api/case/export`, { method: 'get', responseType: 'blob' }); +}; export const exportCases = async (cases: any) => { - return request(`/api/case/export`, { method: 'get', params: { case: cases }, responseType: 'blob' }) -} - + return request(`/api/case/export`, { + method: 'get', + params: { case: cases }, + responseType: 'blob', + }); +}; /* 批量编辑 */ export const batchEditCase = async (data: any) => { - return request(`/api/case/edit`, { method: "post", data }) -} + return request(`/api/case/edit`, { method: 'post', data }); +};