From 59da4f4ebc493c3d3cae3811d063cf961554e35c Mon Sep 17 00:00:00 2001 From: "xinxin.wu" Date: Wed, 10 Jul 2024 21:45:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=B5=8B=E8=AF=95=E8=AE=A1=E5=88=92):=20?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E8=AE=A1=E5=88=92=E5=85=B3=E8=81=94=E7=94=A8?= =?UTF-8?q?=E4=BE=8B=E9=80=89=E6=8B=A9=E4=BA=A4=E4=BA=92&=E5=85=B3?= =?UTF-8?q?=E8=81=94=E7=94=A8=E4=BE=8B=E8=81=94=E8=B0=83=5F=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.vue | 16 +- .../ms-associate-case/apiCaseTable.vue | 30 ++ .../business/ms-associate-case/apiTable.vue | 32 +- .../business/ms-associate-case/caseTable.vue | 72 +++- .../business/ms-associate-case/caseTree.vue | 127 +++++- .../business/ms-associate-case/index.vue | 385 +++++++++++++++--- .../ms-associate-case/locale/en-US.ts | 10 + .../ms-associate-case/locale/zh-CN.ts | 10 + .../ms-associate-case/scenarioCaseTable.vue | 40 +- .../business/ms-associate-case/totalCount.vue | 21 + .../business/ms-associate-case/types.ts | 13 + .../ms-associate-case/useModuleSelection.ts | 217 ++++++++++ .../testPlanMinder/associateDrawer.vue | 11 +- .../ms-minders/testPlanMinder/index.vue | 36 +- .../src/components/business/ms-tree/index.vue | 11 +- .../components/pure/ms-table/base-table.vue | 10 +- .../components/pure/ms-table/batchAction.vue | 4 +- frontend/src/models/testPlan/testPlan.ts | 11 + 18 files changed, 912 insertions(+), 144 deletions(-) create mode 100644 frontend/src/components/business/ms-associate-case/totalCount.vue create mode 100644 frontend/src/components/business/ms-associate-case/types.ts create mode 100644 frontend/src/components/business/ms-associate-case/useModuleSelection.ts diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 98325424ce..b818a8a3bc 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,8 +1,6 @@ @@ -46,8 +44,6 @@ } }); - const loading = ref(false); - // 初始化平台风格和主题色 watchStyle(appStore.pageConfig.style, appStore.pageConfig); watchTheme(appStore.pageConfig.theme, appStore.pageConfig); @@ -94,24 +90,26 @@ state.value = getQueryVariable('state') || ''; if (state.value.split('#')[0] === 'fit2cloud-lark-qr') { try { - loading.value = true; + appStore.showLoading(); const larkCallback = await getLarkCallback(code || ''); userStore.qrCodeLogin(larkCallback); setLoginExpires(); - loading.value = false; } catch (err) { console.log(err); + } finally { + appStore.hideLoading(); } } if (state.value.split('#')[0] === 'fit2cloud-lark-suite-qr') { try { - loading.value = true; + appStore.showLoading(); const larkCallback = await getLarkSuiteCallback(code || ''); userStore.qrCodeLogin(larkCallback); setLoginExpires(); - loading.value = false; } catch (err) { console.log(err); + } finally { + appStore.hideLoading(); } } await userStore.checkIsLogin(); diff --git a/frontend/src/components/business/ms-associate-case/apiCaseTable.vue b/frontend/src/components/business/ms-associate-case/apiCaseTable.vue index 100159b18b..a23cfc2e68 100644 --- a/frontend/src/components/business/ms-associate-case/apiCaseTable.vue +++ b/frontend/src/components/business/ms-associate-case/apiCaseTable.vue @@ -8,6 +8,9 @@ moreAction: [], }" v-on="propsEvent" + @row-select-change="rowSelectChange" + @select-all-change="selectAllChange" + @clear-selector="clearSelector" > @@ -54,6 +60,7 @@ import useTable from '@/components/pure/ms-table/useTable'; import CaseLevel from '@/components/business/ms-case-associate/caseLevel.vue'; import ExecuteResult from '@/components/business/ms-case-associate/executeResult.vue'; + import type { MsTreeNodeData } from '@/components/business/ms-tree/types'; import ExecutionStatus from '@/views/api-test/report/component/reportStatus.vue'; import useOpenNewPage from '@/hooks/useOpenNewPage'; @@ -69,6 +76,8 @@ import { SpecialColumnEnum, TableKeyEnum } from '@/enums/tableEnum'; import { FilterSlotNameEnum } from '@/enums/tableFilterEnum'; + import type { moduleKeysType } from './types'; + import useModuleSelection from './useModuleSelection'; import { getPublicLinkCaseListMap } from './utils/page'; import { casePriorityOptions } from '@/views/api-test/components/config'; @@ -87,6 +96,7 @@ getPageApiType: keyof typeof CasePageApiTypeEnum; // 获取未关联分页Api extraTableParams?: TableQueryParams; // 查询表格的额外参数 protocols: string[]; + moduleTree: MsTreeNodeData[]; }>(); const emit = defineEmits<{ @@ -96,6 +106,10 @@ (e: 'update:selectedIds'): void; }>(); + const innerSelectedModulesMaps = defineModel>('selectedModulesMaps', { + required: true, + }); + const tableStore = useTableStore(); const lastReportStatusListOptions = computed(() => { @@ -332,6 +346,22 @@ }); } + const { rowSelectChange, selectAllChange, clearSelector, setModuleTree } = useModuleSelection( + innerSelectedModulesMaps.value, + propsRes.value, + props.moduleTree + ); + + watch( + () => props.moduleTree, + (val) => { + setModuleTree(val); + }, + { + immediate: true, + } + ); + defineExpose({ getApiCaseSaveParams, loadCaseList, diff --git a/frontend/src/components/business/ms-associate-case/apiTable.vue b/frontend/src/components/business/ms-associate-case/apiTable.vue index 9efd501f2e..c4f63661dc 100644 --- a/frontend/src/components/business/ms-associate-case/apiTable.vue +++ b/frontend/src/components/business/ms-associate-case/apiTable.vue @@ -10,6 +10,9 @@ }" v-on="propsEvent" @filter-change="getModuleCount" + @row-select-change="rowSelectChange" + @select-all-change="selectAllChange" + @clear-selector="clearSelector" > + @@ -36,6 +42,7 @@ import MsBaseTable from '@/components/pure/ms-table/base-table.vue'; import { MsTableColumn } from '@/components/pure/ms-table/type'; import useTable from '@/components/pure/ms-table/useTable'; + import type { MsTreeNodeData } from '@/components/business/ms-tree/types'; import apiMethodName from '@/views/api-test/components/apiMethodName.vue'; import { useI18n } from '@/hooks/useI18n'; @@ -52,6 +59,8 @@ import { SpecialColumnEnum, TableKeyEnum } from '@/enums/tableEnum'; import { FilterRemoteMethodsEnum, FilterSlotNameEnum } from '@/enums/tableFilterEnum'; + import type { moduleKeysType } from './types'; + import useModuleSelection from './useModuleSelection'; import { getPublicLinkCaseListMap } from './utils/page'; const { t } = useI18n(); @@ -71,6 +80,7 @@ getPageApiType: keyof typeof CasePageApiTypeEnum; // 获取未关联分页Api extraTableParams?: TableQueryParams; // 查询表格的额外参数 protocols: string[]; + moduleTree: MsTreeNodeData[]; }>(); const emit = defineEmits<{ @@ -81,6 +91,9 @@ }>(); const tableStore = useTableStore(); + const innerSelectedModulesMaps = defineModel>('selectedModulesMaps', { + required: true, + }); const requestMethodsOptions = computed(() => { return Object.values(RequestMethods).map((e) => { @@ -180,10 +193,10 @@ propsEvent, loadList, setLoadListParams, - resetSelector, setPagination, resetFilterParams, setTableSelected, + resetSelector, } = useTable(getPublicLinkCaseListMap[props.getPageApiType][props.activeSourceType].API, { tableKey: TableKeyEnum.ASSOCIATE_CASE_API, showSetting: true, @@ -239,7 +252,6 @@ setPagination({ current: 1, }); - resetSelector(); resetFilterParams(); loadApiList(); } @@ -299,6 +311,22 @@ }); } + const { rowSelectChange, selectAllChange, clearSelector, setModuleTree } = useModuleSelection( + innerSelectedModulesMaps.value, + propsRes.value, + props.moduleTree + ); + + watch( + () => props.moduleTree, + (val) => { + setModuleTree(val); + }, + { + immediate: true, + } + ); + defineExpose({ getApiSaveParams, loadApiList, diff --git a/frontend/src/components/business/ms-associate-case/caseTable.vue b/frontend/src/components/business/ms-associate-case/caseTable.vue index 1386336772..70730ed7f2 100644 --- a/frontend/src/components/business/ms-associate-case/caseTable.vue +++ b/frontend/src/components/business/ms-associate-case/caseTable.vue @@ -9,6 +9,9 @@ }" v-on="propsEvent" @filter-change="getModuleCount" + @row-select-change="rowSelectChange" + @select-all-change="selectAllChange" + @clear-selector="clearSelector" > @@ -51,6 +57,7 @@ import useTable from '@/components/pure/ms-table/useTable'; import CaseLevel from '@/components/business/ms-case-associate/caseLevel.vue'; import ExecuteResult from '@/components/business/ms-case-associate/executeResult.vue'; + import type { MsTreeNodeData } from '@/components/business/ms-tree/types'; import useOpenNewPage from '@/hooks/useOpenNewPage'; import useTableStore from '@/hooks/useTableStore'; @@ -63,6 +70,8 @@ import { SpecialColumnEnum, TableKeyEnum } from '@/enums/tableEnum'; import { FilterSlotNameEnum } from '@/enums/tableFilterEnum'; + import type { moduleKeysType } from './types'; + import useModuleSelection from './useModuleSelection'; import { getPublicLinkCaseListMap } from './utils/page'; import { casePriorityOptions } from '@/views/api-test/components/config'; import { executionResultMap, statusIconMap } from '@/views/case-management/caseManagementFeature/components/utils'; @@ -77,6 +86,7 @@ associatedIds?: string[]; // 已关联ids activeSourceType: keyof typeof CaseLinkEnum; keyword: string; + moduleTree: MsTreeNodeData[]; getPageApiType: keyof typeof CasePageApiTypeEnum; // 获取未关联分页Api extraTableParams?: TableQueryParams; // 查询表格的额外参数 }>(); @@ -86,12 +96,17 @@ (e: 'refresh'): void; (e: 'initModules'): void; (e: 'update:selectedIds'): void; + (e: 'clearSelect'): void; }>(); const tableStore = useTableStore(); const innerSelectedIds = defineModel('selectedIds', { required: true }); + const innerSelectedModulesMaps = defineModel>('selectedModulesMaps', { + required: true, + }); + const reviewResultOptions = computed(() => { return Object.keys(statusIconMap).map((key) => { return { @@ -218,26 +233,25 @@ return undefined; } - const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, resetFilterParams, setTableSelected } = - useTable( - getPageList.value, - { - tableKey: TableKeyEnum.ASSOCIATE_CASE, - showSetting: true, - isSimpleSetting: true, - onlyPageSize: true, - selectable: true, - showSelectAll: true, - heightUsed: 310, - showSelectorAll: false, - }, - (record) => { - return { - ...record, - caseLevel: getCaseLevel(record), - }; - } - ); + const { propsRes, propsEvent, loadList, setLoadListParams, resetFilterParams, setTableSelected } = useTable( + getPageList.value, + { + tableKey: TableKeyEnum.ASSOCIATE_CASE, + showSetting: true, + isSimpleSetting: true, + onlyPageSize: true, + selectable: true, + showSelectAll: true, + heightUsed: 310, + showSelectorAll: false, + }, + (record) => { + return { + ...record, + caseLevel: getCaseLevel(record), + }; + } + ); async function getTableParams() { const { excludeKeys } = propsRes.value; @@ -253,6 +267,7 @@ async function getModuleCount() { const tableParams = await getTableParams(); + // 这里的count始终都是全量的 emit('getModuleCount', { ...tableParams, current: propsRes.value.msPagination?.current, @@ -301,6 +316,22 @@ return [...propsRes.value.selectedKeys]; }); + const { rowSelectChange, selectAllChange, clearSelector, setModuleTree } = useModuleSelection( + innerSelectedModulesMaps.value, + propsRes.value, + props.moduleTree + ); + + watch( + () => props.moduleTree, + (val) => { + setModuleTree(val); + }, + { + immediate: true, + } + ); + watch( () => selectIds.value, (val) => { @@ -309,7 +340,6 @@ ); watch([() => props.currentProject, () => props.activeModule], () => { - resetSelector(); resetFilterParams(); loadCaseList(); }); diff --git a/frontend/src/components/business/ms-associate-case/caseTree.vue b/frontend/src/components/business/ms-associate-case/caseTree.vue index 4f900d2f2a..e2a7b5330f 100644 --- a/frontend/src/components/business/ms-associate-case/caseTree.vue +++ b/frontend/src/components/business/ms-associate-case/caseTree.vue @@ -28,10 +28,21 @@ - +
+ {{ + t('ms.case.associate.allData') + }} + + {{ allCount }} + +
+ + @@ -60,6 +87,7 @@ import { ref } from 'vue'; import { useVModel } from '@vueuse/core'; + import MsButton from '@/components/pure/ms-button/index.vue'; import MsTree from '@/components/business/ms-tree/index.vue'; import type { MsTreeNodeData } from '@/components/business/ms-tree/types'; import TreeFolderAll from '@/views/api-test/components/treeFolderAll.vue'; @@ -76,7 +104,7 @@ const { t } = useI18n(); const props = defineProps<{ - modulesCount?: Record; // 模块数量统计对象 + modulesCount: Record; // 模块数量统计对象 selectedKeys: string[]; // 选中的节点 key currentProject: string; getModulesApiType: CaseModulesApiTypeEnum[keyof CaseModulesApiTypeEnum]; @@ -91,9 +119,25 @@ (e: 'init', params: ModuleTreeNode[], selectedProtocols?: string[]): void; (e: 'changeProtocol', selectedProtocols: string[]): void; (e: 'update:selectedKeys', selectedKeys: string[]): void; + (e: 'update:halfCheckedKeys', halfCheckedKeys: string[]): void; + (e: 'check', _checkedKeys: Array, checkedNodes: MsTreeNodeData): void; + (e: 'selectParent', node: MsTreeNodeData, isSelected: boolean): void; + (e: 'checkAllModule', isCheckedAll: boolean): void; }>(); const selectedKeys = useVModel(props, 'selectedKeys', emit); + const checkedKeys = defineModel<(string | number)[]>('checkedKeys', { + required: true, + }); + const halfCheckedKeys = defineModel<(string | number)[]>('halfCheckedKeys', { + required: true, + }); + const isCheckAll = defineModel('isCheckedAll', { + required: true, + }); + const indeterminate = defineModel('indeterminate', { + required: true, + }); const moduleKeyword = ref(''); const activeFolder = ref('all'); const allCount = ref(0); @@ -103,7 +147,7 @@ const virtualListProps = computed(() => { return { - height: 'calc(100vh - 180px)', + height: 'calc(100vh - 236px)', threshold: 200, fixedSize: true, buffer: 15, // 缓冲区默认 10 的时候,虚拟滚动的底部 padding 计算有问题 @@ -129,6 +173,42 @@ } const selectedProtocols = ref([]); + // 递归计算并更新节点的 count + function processTreeData(nodes: MsTreeNodeData[]): MsTreeNodeData[] { + const traverse = (node: MsTreeNodeData): number => { + let totalChildrenCount = 0; + + if (node.children && node.children.length > 0) { + totalChildrenCount = node.children.reduce((sum, child) => { + return sum + traverse(child); + }, 0); + node.count -= totalChildrenCount; + } + + return node.count + totalChildrenCount; + }; + + nodes.forEach((node: MsTreeNodeData) => traverse(node)); + return nodes; + } + + function calculateTreeCount(treeData: MsTreeNodeData[]) { + caseTree.value = mapTree(treeData, (node) => { + return { + ...node, + count: props.modulesCount[node.id], + }; + }); + + const updatedModuleTreeCount = processTreeData(caseTree.value) as MsTreeNodeData[]; + caseTree.value = mapTree(updatedModuleTreeCount, (node) => { + return { + ...node, + count: node.count, + }; + }); + allCount.value = props.modulesCount.all || 0; + } /** * 初始化模块树 */ @@ -143,15 +223,13 @@ }; const res = await getModuleTreeFunc(props.getModulesApiType, props.activeTab, getModuleParams); - caseTree.value = mapTree(res, (node) => { - return { - ...node, - count: props.modulesCount?.[node.id] || 0, - }; - }); + + calculateTreeCount(res); + if (setDefault) { setActiveFolder('all'); } + emit('init', caseTree.value, selectedProtocols.value); } catch (error) { // eslint-disable-next-line no-console @@ -166,22 +244,33 @@ initModules(); } + function checkNode(_checkedKeys: Array, checkedNodes: MsTreeNodeData) { + emit('check', _checkedKeys, checkedNodes); + } + + function selectCurrent(node: MsTreeNodeData, isSelected: boolean) { + emit('selectParent', node, isSelected); + } + /** * 初始化模块文件数量 */ watch( () => props.modulesCount, - (obj) => { - caseTree.value = mapTree(caseTree.value, (node) => { - return { - ...node, - count: obj?.[node.id] || 0, - }; - }); - allCount.value = obj?.all || 0; + () => { + calculateTreeCount(caseTree.value); + }, + { + deep: true, + immediate: true, } ); + function handleChangeAll(value: boolean | (string | number | boolean)[], ev: Event) { + isCheckAll.value = value as boolean; + emit('checkAllModule', isCheckAll.value); + } + watch( () => props.showType, (val) => { diff --git a/frontend/src/components/business/ms-associate-case/index.vue b/frontend/src/components/business/ms-associate-case/index.vue index 7595a0e9c4..03edda0595 100644 --- a/frontend/src/components/business/ms-associate-case/index.vue +++ b/frontend/src/components/business/ms-associate-case/index.vue @@ -113,11 +113,15 @@ -
+
-
+
- -
@@ -186,6 +181,7 @@ v-if="associationType === CaseLinkEnum.FUNCTIONAL" ref="functionalTableRef" v-model:selectedIds="selectedIds" + v-model:selectedModulesMaps="selectedModulesMaps" :association-type="associateType" :get-page-api-type="getPageApiType" :active-module="activeFolder" @@ -195,14 +191,18 @@ :active-source-type="associationType" :extra-table-params="props.extraTableParams" :keyword="keyword" + :module-tree="moduleTree" @get-module-count="initModulesCount" @refresh="loadCaseList" - /> + > + + + > + + + > + + + > + + +
+
+
-
{{ t('msTable.batch.selected', { count: props.selectRowCount }) }}
+ +
{{ t('msTable.batch.selected', { count: props.selectRowCount }) }}
+