feat(工作台): 我关注的-页面

This commit is contained in:
baiqi 2024-11-06 17:16:10 +08:00 committed by Craftsman
parent 68f5fb6624
commit b4990750d1
31 changed files with 1667 additions and 51 deletions

View File

@ -1,10 +1,13 @@
<template>
<a-select class="w-[260px]" :default-value="innerProject" allow-search @change="selectProject">
<template #arrow-icon>
<a-select :class="props.class || 'w-[260px]'" :default-value="project" allow-search @change="selectProject">
<template v-if="!props.useDefaultArrowIcon" #arrow-icon>
<icon-caret-down />
</template>
<template v-if="$slots.prefix" #prefix>
<slot name="prefix"></slot>
</template>
<a-tooltip v-for="item of projectList" :key="item.id" :mouse-enter-delay="500" :content="item.name">
<a-option :value="item.id" :class="item.id === innerProject ? 'arco-select-option-selected' : ''">
<a-option :value="item.id" :class="item.id === project ? 'arco-select-option-selected' : ''">
{{ item.name }}
</a-option>
</a-tooltip>
@ -18,32 +21,23 @@
import type { ProjectListItem } from '@/models/setting/project';
const props = defineProps<{
project: string;
class?: string;
useDefaultArrowIcon?: boolean;
}>();
const emit = defineEmits<{
(e: 'update:project', val: string): void;
(e: 'change', val: string): void;
}>();
const appStore = useAppStore();
const projectList = ref<ProjectListItem[]>([]);
const innerProject = ref(props.project || appStore.currentProjectId);
watch(
() => props.project,
(val) => {
innerProject.value = val;
}
);
watch(
() => innerProject.value,
(val) => {
emit('update:project', val);
}
);
const project = defineModel<string>('project', {
default: () => '',
});
onBeforeMount(async () => {
if (!project.value) {
project.value = appStore.currentProjectId;
}
try {
if (appStore.currentOrgId) {
const res = await getProjectList(appStore.getCurrentOrgId);
@ -60,7 +54,6 @@
function selectProject(
value: string | number | boolean | Record<string, any> | (string | number | boolean | Record<string, any>)[]
) {
emit('update:project', value as string);
emit('change', value as string);
}
</script>

View File

@ -186,6 +186,7 @@
count?: number;
notShowInputSearch?: boolean;
viewType?: ViewTypeEnum;
viewName?: string;
}>();
const emit = defineEmits<{
@ -201,7 +202,7 @@
const visible = ref(false);
const filterResult = ref<FilterResult>({ searchMode: 'AND', conditions: [] });
const currentView = ref(''); //
const currentView = ref(props.viewName || ''); //
const internalViews = ref<ViewItem[]>([]);
const customViews = ref<ViewItem[]>([]);
const viewListLoading = ref(false);
@ -228,12 +229,20 @@
value: e.id,
}));
}
const isAdvancedSearchMode = ref(false);
onMounted(async () => {
if (props.viewType) {
getMemberOptions();
await getUserViewList();
if (props.viewName) {
currentView.value = props.viewName;
isAdvancedSearchMode.value = currentView.value !== internalViews.value[0].id;
} else {
currentView.value = internalViews.value[0]?.id;
}
}
});
watch(
@ -325,7 +334,6 @@
}
}
const isAdvancedSearchMode = ref(false);
const getIsValidValue = (item: ConditionsItem) => {
if (typeof item.value === 'boolean') return String(item.value).length;
if (typeof item.value === 'number') return item.value;

View File

@ -183,7 +183,7 @@
}
const width = props.otherWidth
? `calc(100vw - ${menuWidth.value}px - ${props.otherWidth}px)`
: `calc(100vw - ${menuWidth.value}px - 58px)`;
: `calc(100vw - ${menuWidth.value}px - 48px)`; // 48px 32+ 16
return {
overflow: 'auto',
width: props.autoWidth ? 'auto' : width,

View File

@ -106,13 +106,6 @@
const isExpanded = ref(true);
const isExpandAnimating = ref(false); //
watch(
() => props.notShowFirst,
(val) => {
innerSize.value = val ? 0 : initialSize;
}
);
watch(
() => props.size,
(val) => {
@ -159,6 +152,18 @@
}
}
watch(
() => props.notShowFirst,
(val) => {
if (val) {
collapse();
} else {
expand();
}
},
{ immediate: true }
);
defineExpose({
expand,
collapse,

View File

@ -57,4 +57,11 @@ export enum WorkCardEnum {
BUG_HANDLE_USER = 'BUG_HANDLE_USER', // 缺陷处理人统计
}
export default {};
export enum FeatureEnum {
TEST_PLAN = 'TEST_PLAN',
TEST_CASE = 'TEST_CASE',
CASE_REVIEW = 'CASE_REVIEW',
API_CASE = 'API_CASE',
API_SCENARIO = 'API_SCENARIO',
BUG = 'BUG',
}

View File

@ -44,7 +44,7 @@ const TestPlan: AppRouteRecordRaw = {
{
path: 'followed',
name: WorkbenchRouteEnum.WORKBENCH_INDEX_FOLLOW,
component: () => import('@/views/workbench/homePage/index.vue'),
component: () => import('@/views/workbench/myFollowed/index.vue'),
meta: {
locale: 'menu.workbenchFollowSort',
roles: ['*'],

View File

@ -913,6 +913,7 @@ export function customFieldToColumns(customFields: CustomFieldItem[]) {
width: 200,
options: options || JSON.parse(platformOptionJson),
type,
internal: field.internal,
};
return column;
});

View File

@ -40,7 +40,7 @@
import useTable from '@/components/pure/ms-table/useTable';
import { deleteDefinitionSchedule, switchDefinitionSchedule } from '@/api/modules/api-test/management';
import { getScheduleProApiCaseList, projectDeleteSchedule } from '@/api/modules/taskCenter/project';
import { getScheduleProApiCaseList } from '@/api/modules/taskCenter/project';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import { characterLimit } from '@/utils';

View File

@ -540,8 +540,7 @@
});
}
await initFilterColumn();
await tableStore.initColumn(TableKeyEnum.API_TEST, columns, 'drawer', true);
initFilterColumn();
if (props.readOnly) {
columns = columns.filter(
(item) => !['version', 'createTime', 'updateTime', 'operation'].includes(item.dataIndex as string)
@ -1137,6 +1136,12 @@
initFilterColumn();
}
);
defineExpose({
isAdvancedSearchMode,
});
await tableStore.initColumn(TableKeyEnum.API_TEST, columns, 'drawer', true);
</script>
<style lang="less" scoped>

View File

@ -3,6 +3,7 @@
<keep-alive :include="cacheStore.cacheViews">
<apiTable
v-if="activeApiTab.id === 'all' && currentTab === 'api'"
ref="apiTableRef"
class="flex-1 pt-[8px]"
:active-module="props.activeModule"
:offspring-ids="props.offspringIds"
@ -577,10 +578,13 @@
}
});
const apiTableRef = ref<InstanceType<typeof apiTable>>();
const isAdvancedSearchMode = computed(() => apiTableRef.value?.isAdvancedSearchMode);
defineExpose({
openApiTab,
addApiTab,
openApiTabAndDebugMock,
refreshTable,
isAdvancedSearchMode,
});
</script>

View File

@ -15,6 +15,7 @@
:view-type="ViewTypeEnum.API_CASE"
:filter-config-list="filterConfigList"
:search-placeholder="t('apiTestManagement.searchPlaceholder')"
:view-name="viewName"
@keyword-search="loadCaseList()"
@adv-search="handleAdvSearch"
@refresh="loadCaseList()"
@ -309,6 +310,7 @@
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router';
import { FormInstance, Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
@ -391,6 +393,7 @@
(e: 'handleAdvSearch', isStartAdvance: boolean): void;
}>();
const route = useRoute();
const appStore = useAppStore();
const { t } = useI18n();
const tableStore = useTableStore();
@ -803,6 +806,9 @@
type: FilterType.DATE_PICKER,
},
]);
const viewName = ref('');
//
const handleAdvSearch = async (filter: FilterResult, id: string, isStartAdvance: boolean) => {
resetSelector();
@ -1171,8 +1177,16 @@
createAndEditCaseDrawerRef.value?.open(caseDetail.value.apiDefinitionId, caseDetail.value as RequestParam, false);
}
onBeforeMount(() => {
if (route.query.view) {
setAdvanceFilter({}, route.query.view as string);
viewName.value = route.query.view as string;
}
});
defineExpose({
loadCaseList,
isAdvancedSearchMode,
});
await tableStore.initColumn(TableKeyEnum.API_TEST_MANAGEMENT_CASE, columns, 'drawer', true);

View File

@ -156,8 +156,10 @@
}
});
const isAdvancedSearchMode = computed(() => caseTableRef.value?.isAdvancedSearchMode);
defineExpose({
openCaseTab,
openCaseTabAndExecute,
isAdvancedSearchMode,
});
</script>

View File

@ -379,6 +379,10 @@
'PROJECT_API_DEFINITION_CASE:READ+UPDATE',
]);
const isAdvancedSearchMode = computed(() =>
currentTab.value === 'api' ? apiRef.value?.isAdvancedSearchMode : caseRef.value?.isAdvancedSearchMode
);
/** 向孙组件提供属性 */
provide('defaultCaseParams', readonly(defaultCaseParams));
provide('protocols', readonly(protocols));
@ -390,6 +394,7 @@
handleApiUpdateFromModuleTree,
handleDeleteApiFromModuleTree,
changeActiveApiTabToFirst,
isAdvancedSearchMode,
});
</script>

View File

@ -196,9 +196,8 @@
}
};
const isAdvancedSearchMode = ref(false);
function handleAdvSearch(isStartAdvance: boolean) {
isAdvancedSearchMode.value = isStartAdvance;
const isAdvancedSearchMode = computed(() => managementRef.value?.isAdvancedSearchMode);
function handleAdvSearch() {
moduleTreeRef.value?.setActiveFolder('all');
}

View File

@ -6,6 +6,7 @@
:view-type="ViewTypeEnum.API_SCENARIO"
:filter-config-list="filterConfigList"
:search-placeholder="t('api_scenario.table.searchPlaceholder')"
:view-name="viewName"
@keyword-search="loadScenarioList(true)"
@adv-search="handleAdvSearch"
@refresh="loadScenarioList(true)"
@ -481,6 +482,7 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useRoute } from 'vue-router';
import { FormInstance, Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import dayjs from 'dayjs';
@ -559,6 +561,7 @@
(e: 'handleAdvSearch', isStartAdvance: boolean): void;
}>();
const route = useRoute();
const appStore = useAppStore();
const cacheStore = useCacheStore();
const showExportModal = ref(false);
@ -1071,6 +1074,9 @@
type: FilterType.DATE_PICKER,
},
]);
const viewName = ref('');
//
const handleAdvSearch = async (filter: FilterResult, id: string, isStartAdvance: boolean) => {
resetSelector();
@ -1583,6 +1589,7 @@
defineExpose({
loadScenarioList,
isAdvancedSearchMode,
});
if (!props.readOnly) {
@ -1617,6 +1624,10 @@
onBeforeMount(() => {
cacheStore.clearCache();
if (route.query.view) {
setAdvanceFilter({}, route.query.view as string);
viewName.value = route.query.view as string;
}
if (!isActivated.value) {
loadScenarioList();
cacheStore.setCache(CacheTabTypeEnum.API_SCENARIO_TABLE);

View File

@ -520,9 +520,9 @@
}
const scenarioModuleTreeRef = ref<InstanceType<typeof scenarioModuleTree>>();
const isAdvancedSearchMode = ref(false);
function handleAdvSearch(isStartAdvance: boolean) {
isAdvancedSearchMode.value = isStartAdvance;
const apiTableRef = ref<InstanceType<typeof ScenarioTable>>();
const isAdvancedSearchMode = computed(() => apiTableRef.value?.isAdvancedSearchMode);
function handleAdvSearch() {
scenarioModuleTreeRef.value?.setActiveFolder('all');
}
@ -564,7 +564,6 @@
const createRef = ref<InstanceType<typeof create>>();
const detailRef = ref<InstanceType<typeof detail>>();
const apiTableRef = ref<InstanceType<typeof ScenarioTable>>();
const saveLoading = ref(false);
function handleModuleChange() {

View File

@ -8,6 +8,7 @@
:filter-config-list="filterConfigList"
:custom-fields-config-list="searchCustomFields"
:search-placeholder="t('caseManagement.featureCase.searchByNameAndId')"
:view-name="viewName"
@keyword-search="fetchData()"
@adv-search="handleAdvSearch"
@refresh="searchData()"
@ -593,6 +594,8 @@
type: FilterType.DATE_PICKER,
},
]);
const viewName = ref('');
//
const handleAdvSearch = (filter: FilterResult, id: string) => {
resetSelector();
@ -864,6 +867,10 @@
onBeforeMount(() => {
//
checkSyncStatus();
if (route.query.view) {
setAdvanceFilter({}, route.query.view as string);
viewName.value = route.query.view as string;
}
});
let customColumns: MsTableColumn = [];

View File

@ -17,6 +17,7 @@
:search-placeholder="t('caseManagement.featureCase.searchPlaceholder')"
:count="modulesCount[props.activeFolder] || 0"
:name="moduleNamePath"
:view-name="viewName"
@keyword-search="fetchData"
@adv-search="handleAdvSearch"
@refresh="fetchData()"
@ -1838,6 +1839,7 @@
);
const isActivated = computed(() => cacheStore.cacheViews.includes(RouteEnum.CASE_MANAGEMENT_CASE));
const viewName = ref<string>('');
onBeforeUnmount(() => {
showDetailDrawer.value = false;
@ -1845,9 +1847,16 @@
onMounted(() => {
if (!isActivated.value) {
mountedLoad();
//
showType.value = minderStore.getShowType(MinderKeyEnum.FEATURE_CASE_MINDER);
if (route.query.showType) {
showType.value = route.query.showType as ShowType;
}
if (route.query.view) {
setAdvanceFilter({}, route.query.view as string);
viewName.value = route.query.view as string;
}
mountedLoad();
}
});

View File

@ -7,6 +7,7 @@
:view-type="ViewTypeEnum.CASE_REVIEW"
:filter-config-list="filterConfigList"
:search-placeholder="t('caseManagement.caseReview.list.searchPlaceholder')"
:view-name="viewName"
@keyword-search="searchReview()"
@adv-search="handleAdvSearch"
@refresh="searchReview()"
@ -160,7 +161,7 @@
<script setup lang="ts">
import { onBeforeMount } from 'vue';
import { useRouter } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
import { useVModel } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import dayjs from 'dayjs';
@ -224,6 +225,7 @@
const appStore = useAppStore();
const cacheStore = useCacheStore();
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const { openModal } = useModal();
@ -570,6 +572,8 @@
}
}
const viewName = ref<string>('');
//
const handleAdvSearch = async (filter: FilterResult, id: string, isStartAdvance: boolean) => {
resetSelector();
@ -774,6 +778,10 @@
}
onBeforeMount(() => {
if (route.query.view) {
setAdvanceFilter({}, route.query.view as string);
viewName.value = route.query.view as string;
}
if (!isActivated.value) {
mountedLoad();
searchReview();
@ -789,6 +797,7 @@
defineExpose({
searchReview,
isAdvancedSearchMode,
});
await tableStore.initColumn(TableKeyEnum.CASE_MANAGEMENT_REVIEW, columns, 'drawer', true);

View File

@ -53,6 +53,7 @@
defineOptions({
name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW,
});
const router = useRouter();
type ShowType = 'all' | 'reviewByMe' | 'createByMe';
@ -103,9 +104,8 @@
});
}
const isAdvancedSearchMode = ref(false);
function handleAdvSearch(isStartAdvance: boolean) {
isAdvancedSearchMode.value = isStartAdvance;
const isAdvancedSearchMode = computed(() => reviewTableRef.value?.isAdvancedSearchMode);
function handleAdvSearch() {
folderTreeRef.value?.setActiveFolder('all');
}
</script>

View File

@ -24,8 +24,6 @@
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import { hasAnyPermission } from '@/utils/permission';

View File

@ -5,6 +5,7 @@
:view-type="ViewTypeEnum.TEST_PLAN"
:filter-config-list="filterConfigList"
:search-placeholder="t('common.searchByIDNameTag')"
:view-name="viewName"
@keyword-search="fetchData()"
@adv-search="handleAdvSearch"
@refresh="fetchData()"
@ -1705,12 +1706,18 @@
// }
const isActivated = computed(() => cacheStore.cacheViews.includes(RouteEnum.TEST_PLAN_INDEX));
const viewName = ref('');
onBeforeMount(() => {
if (!isActivated.value) {
if (route.query.groupId) {
showType.value = testPlanTypeEnum.GROUP;
keyword.value = route.query.groupId as string;
} else if (route.query.showType) {
showType.value = route.query.showType as testPlanTypeEnum;
}
if (route.query.view) {
viewName.value = route.query.view as string;
}
fetchData();
}

View File

@ -0,0 +1,226 @@
<template>
<MsCard auto-height simple>
<div class="flex items-center justify-between">
<div class="cursor-pointer font-medium text-[var(--color-text-1)]" @click="goApiCase">
{{ t('ms.workbench.myFollowed.feature.API_CASE') }}
</div>
</div>
<ms-base-table v-bind="propsRes" :first-column-width="44" no-disable filter-icon-align-left v-on="propsEvent">
<template #num="{ record }">
<div class="flex items-center">
<MsButton type="text" @click="openCase(record.id)">
{{ record.num }}
</MsButton>
</div>
</template>
<template #caseLevel="{ record }">
<span class="text-[var(--color-text-2)]"> <caseLevel :case-level="record.priority" /></span>
</template>
<!-- 用例等级 -->
<template #[FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL]="{ filterContent }">
<caseLevel :case-level="filterContent.value" />
</template>
<template #status="{ record }">
<apiStatus :status="record.status" />
</template>
<template #[FilterSlotNameEnum.API_TEST_CASE_API_STATUS]="{ filterContent }">
<apiStatus :status="filterContent.value" />
</template>
<template #createName="{ record }">
<a-tooltip :content="`${record.createName}`" position="tr">
<div class="one-line-text">{{ record.createName }}</div>
</a-tooltip>
</template>
<template #[FilterSlotNameEnum.API_TEST_CASE_API_LAST_EXECUTE_STATUS]="{ filterContent }">
<ExecutionStatus :module-type="ReportEnum.API_REPORT" :status="filterContent.value" />
</template>
<template #lastReportStatus="{ record }">
<ExecutionStatus
:module-type="ReportEnum.API_REPORT"
:status="record.lastReportStatus"
:class="[!record.lastReportId ? '' : 'cursor-pointer']"
@click="showResult(record)"
/>
</template>
<template #passRateColumn>
<div class="flex items-center text-[var(--color-text-3)]">
{{ t('case.passRate') }}
<a-tooltip :content="t('case.passRateTip')" position="right">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</div>
</template>
</ms-base-table>
</MsCard>
<!-- 执行结果抽屉 -->
<caseAndScenarioReportDrawer v-model:visible="showExecuteResult" :report-id="activeReportId" />
</template>
<script setup lang="ts">
import MsButton from '@/components/pure/ms-button/index.vue';
import MsCard from '@/components/pure/ms-card/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import apiStatus from '@/views/api-test/components/apiStatus.vue';
import caseAndScenarioReportDrawer from '@/views/api-test/components/caseAndScenarioReportDrawer.vue';
import ExecutionStatus from '@/views/api-test/report/component/reportStatus.vue';
import { getCasePage } from '@/api/modules/api-test/management';
import { useI18n } from '@/hooks/useI18n';
import useOpenNewPage from '@/hooks/useOpenNewPage';
import { ApiCaseDetail } from '@/models/apiTest/management';
import { ReportEnum, ReportStatus } from '@/enums/reportEnum';
import { ApiTestRouteEnum } from '@/enums/routeEnum';
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
import { casePriorityOptions, caseStatusOptions } from '@/views/api-test/components/config';
const props = defineProps<{
project: string;
}>();
const { t } = useI18n();
const { openNewPage } = useOpenNewPage();
const lastReportStatusListOptions = computed(() => {
return Object.keys(ReportStatus).map((key) => {
return {
value: key,
label: t(ReportStatus[key].label),
};
});
});
const columns: MsTableColumn = [
{
title: 'ID',
dataIndex: 'num',
slotName: 'num',
sortIndex: 1,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
fixed: 'left',
width: 150,
columnSelectorDisabled: true,
},
{
title: 'case.caseName',
dataIndex: 'name',
showTooltip: true,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 180,
columnSelectorDisabled: true,
},
{
title: 'case.caseLevel',
dataIndex: 'priority',
slotName: 'caseLevel',
filterConfig: {
options: casePriorityOptions,
filterSlotName: FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL,
},
width: 100,
showDrag: true,
},
{
title: 'apiTestManagement.apiStatus',
dataIndex: 'status',
slotName: 'status',
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
filterConfig: {
options: caseStatusOptions,
filterSlotName: FilterSlotNameEnum.API_TEST_CASE_API_STATUS,
},
width: 150,
showDrag: true,
},
{
title: 'case.lastReportStatus',
dataIndex: 'lastReportStatus',
slotName: 'lastReportStatus',
filterConfig: {
options: lastReportStatusListOptions.value,
filterSlotName: FilterSlotNameEnum.API_TEST_CASE_API_LAST_EXECUTE_STATUS,
},
showInTable: false,
width: 150,
showDrag: true,
},
{
title: 'case.caseEnvironment',
dataIndex: 'environmentName',
showTooltip: true,
showInTable: false,
width: 150,
showDrag: true,
},
{
title: 'case.tableColumnCreateUser',
slotName: 'createName',
dataIndex: 'createUser',
showInTable: true,
width: 180,
},
{
title: 'case.tableColumnCreateTime',
dataIndex: 'createTime',
showInTable: false,
width: 180,
},
];
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(getCasePage, {
columns,
scroll: { x: '100%' },
showSetting: false,
selectable: false,
showSelectAll: false,
paginationSize: 'mini',
});
const activeReportId = ref('');
const showExecuteResult = ref(false);
async function showResult(record: ApiCaseDetail) {
if (!record.lastReportId) return;
activeReportId.value = record.lastReportId;
showExecuteResult.value = true;
}
function openCase(id: number) {
openNewPage(ApiTestRouteEnum.API_TEST_MANAGEMENT, { cId: id, pId: props.project });
}
function goApiCase() {
openNewPage(ApiTestRouteEnum.API_TEST_MANAGEMENT, {
tab: 'case',
view: 'my_follow',
});
}
function init() {
setLoadListParams({
projectId: props.project,
viewId: 'my_follow',
});
loadList();
}
onBeforeMount(() => {
init();
});
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,243 @@
<template>
<MsCard auto-height simple>
<div class="flex items-center justify-between">
<div class="cursor-pointer font-medium text-[var(--color-text-1)]" @click="goBugList">
{{ t('ms.workbench.myFollowed.feature.BUG') }}
</div>
</div>
<MsBaseTable class="mt-[16px]" v-bind="propsRes" v-on="propsEvent">
<!-- ID -->
<template #num="{ record }">
<a-button type="text" class="px-0 text-[14px] leading-[22px]" @click="handleShowDetail(record.id)">
{{ record.num }}
</a-button>
</template>
<template #relationCaseCount="{ record, rowIndex }">
<a-button type="text" class="px-0" @click="showDetail(record.id, rowIndex, 'case')">
{{ record.relationCaseCount }}
</a-button>
</template>
<template #statusName="{ record }">
{{ record.statusName || '-' }}
</template>
<template #handleUserTitle>
<div class="flex items-center text-[var(--color-text-3)]">
{{ t('bugManagement.handleMan') }}
<a-tooltip :content="t('bugManagement.handleManTips')" position="right">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</div>
</template>
</MsBaseTable>
</MsCard>
</template>
<script setup lang="ts">
import { TableData } from '@arco-design/web-vue';
import MsCard from '@/components/pure/ms-card/index.vue';
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 { getBugList, getCustomFieldHeader, getCustomOptionHeader } from '@/api/modules/bug-management';
import { useI18n } from '@/hooks/useI18n';
import useOpenNewPage from '@/hooks/useOpenNewPage';
import useAppStore from '@/store/modules/app';
import { customFieldDataToTableData, customFieldToColumns } from '@/utils';
import { BugEditCustomField, BugOptionItem } from '@/models/bug-management';
import { BugManagementRouteEnum } from '@/enums/routeEnum';
import { makeColumns } from '@/views/case-management/caseManagementFeature/components/utils';
const props = defineProps<{
project: string;
}>();
const appStore = useAppStore();
const { t } = useI18n();
const { openNewPage } = useOpenNewPage();
let columns: MsTableColumn = [
{
title: 'bugManagement.ID',
dataIndex: 'num',
slotName: 'num',
width: 100,
showInTable: true,
columnSelectorDisabled: true,
},
{
title: 'bugManagement.bugName',
dataIndex: 'title',
width: 250,
showTooltip: true,
showInTable: true,
},
{
title: 'bugManagement.status',
dataIndex: 'status',
width: 100,
showTooltip: false,
slotName: 'statusName',
filterConfig: {
options: [],
labelKey: 'text',
},
showInTable: true,
},
{
title: 'bugManagement.creator',
dataIndex: 'createUser',
slotName: 'createUser',
width: 125,
showTooltip: true,
showDrag: true,
filterConfig: {
options: [],
labelKey: 'text',
},
showInTable: true,
},
{
title: 'bugManagement.createTime',
dataIndex: 'createTime',
showDrag: true,
width: 199,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
showInTable: true,
},
{
title: 'bugManagement.updateUser',
dataIndex: 'updateUser',
width: 125,
showTooltip: true,
showDrag: true,
filterConfig: {
options: [],
labelKey: 'text',
},
showInTable: true,
},
{
title: 'bugManagement.updateTime',
dataIndex: 'updateTime',
showDrag: true,
width: 199,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
showInTable: true,
},
];
const statusOption = ref<BugOptionItem[]>([]);
async function initFilterOptions() {
const res = await getCustomOptionHeader(appStore.currentProjectId);
statusOption.value = res.statusOption;
const filterOptionsMaps: Record<string, any> = {
status: res.statusOption,
createUser: res.userOption,
updateUser: res.userOption,
};
columns = makeColumns(filterOptionsMaps, columns);
}
//
const customFields = ref<BugEditCustomField[]>([]);
//
const getCustomFieldColumns = async () => {
const res = await getCustomFieldHeader(props.project);
customFields.value = res;
return customFieldToColumns(res);
};
let customColumns: MsTableColumn = [];
async function getColumnHeaders() {
try {
const res = await getCustomFieldColumns();
customColumns = res.filter((item) => {
//
if ((item.title === '严重程度' || item.title === 'Bug Degree') && item.internal) {
item.showInTable = true;
item.slotName = 'severity';
item.filterConfig = {
options: item.options || [],
labelKey: 'text',
};
}
return (item.title === '严重程度' || item.title === 'Bug Degree') && item.internal;
});
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
await getColumnHeaders();
columns.splice(2, 0, ...customColumns);
await initFilterOptions();
const { propsRes, propsEvent, setLoadListParams, loadList } = useTable(
getBugList,
{
columns,
selectable: false,
noDisable: false,
showSetting: false,
paginationSize: 'mini',
},
(record: TableData) => ({
...record,
createUser: record.createUserName,
handleUser: record.handleUserName,
updateUser: record.updateUserName,
...customFieldDataToTableData(record.customFields, customFields.value),
})
);
const detailVisible = ref(false);
const activeDetailId = ref<string>('');
const activeCaseIndex = ref<number>(0);
const activeDetailTab = ref<string>('');
const showDetail = (id: string, rowIndex: number, tab: string) => {
activeDetailId.value = id;
activeCaseIndex.value = rowIndex;
activeDetailTab.value = tab;
detailVisible.value = true;
};
function handleShowDetail(id: number) {
openNewPage(BugManagementRouteEnum.BUG_MANAGEMENT_DETAIL, { id, pId: props.project });
}
function goBugList() {
openNewPage(BugManagementRouteEnum.BUG_MANAGEMENT_INDEX, {
view: 'my_follow',
});
}
function init() {
setLoadListParams({
projectId: props.project,
viewId: 'my_follow',
});
loadList();
}
onBeforeMount(() => {
init();
});
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,188 @@
<template>
<MsCard auto-height simple>
<div class="flex items-center justify-between">
<div class="cursor-pointer font-medium text-[var(--color-text-1)]" @click="goCaseReview">
{{ t('ms.workbench.myFollowed.feature.CASE_REVIEW') }}
</div>
</div>
<ms-base-table v-bind="propsRes" no-disable filter-icon-align-left v-on="propsEvent">
<template #[FilterSlotNameEnum.CASE_MANAGEMENT_REVIEW_STATUS]="{ filterContent }">
<a-tag
:color="reviewStatusMap[filterContent.value as ReviewStatus].color"
:class="[reviewStatusMap[filterContent.value as ReviewStatus].class, 'px-[4px]']"
size="small"
>
{{ t(reviewStatusMap[filterContent.value as ReviewStatus].label) }}
</a-tag>
</template>
<template #passRateColumn>
<div class="flex items-center text-[var(--color-text-3)]">
{{ t('caseManagement.caseReview.passRate') }}
<a-tooltip :content="t('caseManagement.caseReview.passRateTip')" position="right">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</div>
</template>
<template #num="{ record }">
<a-tooltip :content="`${record.num}`">
<a-button type="text" class="px-0 !text-[14px] !leading-[22px]" @click="openDetail(record.id)">
<div class="one-line-text max-w-[168px]">{{ record.num }}</div>
</a-button>
</a-tooltip>
</template>
<template #status="{ record }">
<MsStatusTag :status="record.status" />
</template>
<template #reviewPassRule="{ record }">
<a-tag
:color="record.reviewPassRule === 'SINGLE' ? 'rgb(var(--success-2))' : 'rgb(var(--link-2))'"
:class="record.reviewPassRule === 'SINGLE' ? '!text-[rgb(var(--success-6))]' : '!text-[rgb(var(--link-6))]'"
>
{{
record.reviewPassRule === 'SINGLE'
? t('caseManagement.caseReview.single')
: t('caseManagement.caseReview.multi')
}}
</a-tag>
</template>
<template #passRate="{ record }">
<div class="mr-[8px] w-[100px]">
<passRateLine :review-detail="record" height="5px" />
</div>
<div class="text-[var(--color-text-1)]">
{{ `${record.passRate}%` }}
</div>
</template>
</ms-base-table>
</MsCard>
</template>
<script setup lang="ts">
import MsCard from '@/components/pure/ms-card/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsStatusTag from '@/components/business/ms-status-tag/index.vue';
import passRateLine from '@/views/case-management/caseReview/components/passRateLine.vue';
import { getReviewList } from '@/api/modules/case-management/caseReview';
import { reviewStatusMap } from '@/config/caseManagement';
import { useI18n } from '@/hooks/useI18n';
import useOpenNewPage from '@/hooks/useOpenNewPage';
import { ReviewStatus } from '@/models/caseManagement/caseReview';
import { CaseManagementRouteEnum } from '@/enums/routeEnum';
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
const props = defineProps<{
project: string;
}>();
const { t } = useI18n();
const { openNewPage } = useOpenNewPage();
const reviewStatusOptions = computed(() => {
return Object.keys(reviewStatusMap).map((key) => {
return {
value: key,
label: t(reviewStatusMap[key as ReviewStatus].label),
};
});
});
const columns: MsTableColumn = [
{
title: 'ID',
dataIndex: 'num',
slotName: 'num',
sortIndex: 1,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
showTooltip: true,
width: 100,
},
{
title: 'caseManagement.caseReview.name',
dataIndex: 'name',
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
showTooltip: true,
width: 200,
},
{
title: 'caseManagement.caseReview.status',
dataIndex: 'status',
slotName: 'status',
filterConfig: {
options: reviewStatusOptions.value,
filterSlotName: FilterSlotNameEnum.CASE_MANAGEMENT_REVIEW_STATUS,
},
showDrag: true,
width: 150,
},
{
title: 'caseManagement.caseReview.passRate',
slotName: 'passRate',
titleSlotName: 'passRateColumn',
showDrag: true,
width: 200,
},
{
title: 'caseManagement.caseReview.caseCount',
dataIndex: 'caseCount',
showDrag: true,
width: 100,
},
{
title: 'caseManagement.caseReview.type',
slotName: 'reviewPassRule',
dataIndex: 'reviewPassRule',
showDrag: true,
width: 100,
},
{
title: 'common.createTime',
dataIndex: 'createTime',
width: 180,
},
];
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(getReviewList, {
columns,
showSetting: false,
selectable: false,
showSelectAll: false,
paginationSize: 'mini',
});
function openDetail(id: number) {
openNewPage(CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW_DETAIL, { id });
}
function goCaseReview() {
openNewPage(CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW, {
view: 'my_follow',
});
}
function init() {
setLoadListParams({
projectId: props.project,
viewId: 'my_follow',
});
loadList();
}
onBeforeMount(() => {
init();
});
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,264 @@
<template>
<MsCard auto-height simple>
<div class="flex items-center justify-between">
<div class="cursor-pointer font-medium text-[var(--color-text-1)]" @click="goScenario">
{{ t('ms.workbench.myFollowed.feature.API_SCENARIO') }}
</div>
</div>
<ms-base-table
class="mt-[16px]"
v-bind="propsRes"
:first-column-width="44"
no-disable
filter-icon-align-left
v-on="propsEvent"
>
<template #num="{ record }">
<div class="flex items-center">
<MsButton type="text" class="float-left" style="margin-right: 4px" @click="openScenario(record.id)">
{{ record.num }}
</MsButton>
</div>
</template>
<template #priority="{ record }">
<span class="text-[var(--color-text-2)]"> <caseLevel :case-level="record.priority" /></span>
</template>
<template #[FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL]="{ filterContent }">
<caseLevel :case-level="filterContent.value" />
</template>
<template #[FilterSlotNameEnum.API_TEST_CASE_API_STATUS]="{ filterContent }">
<apiStatus :status="filterContent.value" />
</template>
<template #status="{ record }">
<apiStatus :status="record.status" />
</template>
<template #createUserName="{ record }">
<a-tooltip :content="`${record.createName}`" position="tl">
<div class="one-line-text">{{ characterLimit(record.createUserName) }}</div>
</a-tooltip>
</template>
<!-- 报告结果筛选 -->
<template #[FilterSlotNameEnum.API_TEST_CASE_API_REPORT_STATUS]="{ filterContent }">
<ExecutionStatus :module-type="ReportEnum.API_REPORT" :status="filterContent.value" />
</template>
<template #lastReportStatus="{ record }">
<ExecutionStatus
:module-type="ReportEnum.API_SCENARIO_REPORT"
:status="record.lastReportStatus ? record.lastReportStatus : 'PENDING'"
:script-identifier="record.scriptIdentifier"
:class="record.lastReportId ? 'cursor-pointer' : ''"
@click="openScenarioReportDrawer(record)"
/>
</template>
<template #stepTotal="{ record }">
{{ record.stepTotal }}
</template>
</ms-base-table>
</MsCard>
<!-- 场景报告抽屉 -->
<caseAndScenarioReportDrawer
v-model:visible="showScenarioReportVisible"
is-scenario
:report-id="tableRecord?.lastReportId || ''"
/>
</template>
<script setup lang="ts">
import dayjs from 'dayjs';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsCard from '@/components/pure/ms-card/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import apiStatus from '@/views/api-test/components/apiStatus.vue';
import ExecutionStatus from '@/views/api-test/report/component/reportStatus.vue';
import { getScenarioPage } from '@/api/modules/api-test/scenario';
import { useI18n } from '@/hooks/useI18n';
import useOpenNewPage from '@/hooks/useOpenNewPage';
import { characterLimit } from '@/utils';
import { ApiScenarioTableItem } from '@/models/apiTest/scenario';
import { ApiScenarioStatus } from '@/enums/apiEnum';
import { ReportEnum, ReportStatus } from '@/enums/reportEnum';
import { ApiTestRouteEnum } from '@/enums/routeEnum';
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
import { casePriorityOptions } from '@/views/api-test/components/config';
const props = defineProps<{
project: string;
}>();
const { t } = useI18n();
const { openNewPage } = useOpenNewPage();
const tableRecord = ref<ApiScenarioTableItem>();
const requestApiScenarioStatusOptions = computed(() => {
return Object.values(ApiScenarioStatus).map((key) => {
return {
value: key,
label: key,
};
});
});
const statusList = computed(() => {
return Object.keys(ReportStatus).map((key) => {
return {
value: key,
label: t(ReportStatus[key].label),
};
});
});
const columns: MsTableColumn = [
{
title: 'ID',
dataIndex: 'num',
slotName: 'num',
sortIndex: 1,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
fixed: 'left',
width: 140,
showTooltip: false,
columnSelectorDisabled: true,
},
{
title: 'apiScenario.table.columns.name',
dataIndex: 'name',
slotName: 'name',
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 134,
showTooltip: true,
columnSelectorDisabled: true,
},
{
title: 'apiScenario.table.columns.level',
dataIndex: 'priority',
slotName: 'priority',
showDrag: true,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
filterConfig: {
options: casePriorityOptions,
filterSlotName: FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL,
},
width: 140,
},
{
title: 'apiScenario.table.columns.status',
dataIndex: 'status',
slotName: 'status',
titleSlotName: 'statusFilter',
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
filterConfig: {
options: requestApiScenarioStatusOptions.value,
filterSlotName: FilterSlotNameEnum.API_TEST_CASE_API_STATUS,
},
showDrag: true,
width: 140,
},
{
title: 'apiScenario.table.columns.runResult',
dataIndex: 'lastReportStatus',
slotName: 'lastReportStatus',
showTooltip: false,
showDrag: true,
filterConfig: {
options: statusList.value,
filterSlotName: FilterSlotNameEnum.API_TEST_CASE_API_REPORT_STATUS,
},
width: 200,
},
{
title: 'apiScenario.table.columns.scenarioEnv',
dataIndex: 'environmentName',
showDrag: true,
width: 159,
showTooltip: true,
},
{
title: 'apiScenario.table.columns.createUser',
dataIndex: 'createUser',
slotName: 'createUserName',
showInTable: false,
showTooltip: true,
width: 150,
},
{
title: 'apiScenario.table.columns.createTime',
dataIndex: 'createTime',
showInTable: false,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 180,
showDrag: true,
},
];
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(
getScenarioPage,
{
columns,
scroll: { x: '100%' },
showSetting: false,
selectable: false,
showSelectAll: false,
heightUsed: 282,
paginationSize: 'mini',
},
(item) => ({
...item,
requestPassRate: item.requestPassRate ? `${item.requestPassRate}%` : '-',
createTime: dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss'),
updateTime: dayjs(item.updateTime).format('YYYY-MM-DD HH:mm:ss'),
})
);
const showScenarioReportVisible = ref(false);
function openScenarioReportDrawer(record: ApiScenarioTableItem) {
if (record.lastReportId) {
tableRecord.value = record;
showScenarioReportVisible.value = true;
}
}
function openScenario(id: number) {
openNewPage(ApiTestRouteEnum.API_TEST_SCENARIO, { id, pId: props.project });
}
function goScenario() {
openNewPage(ApiTestRouteEnum.API_TEST_SCENARIO, {
view: 'my_follow',
});
}
function init() {
setLoadListParams({
projectId: props.project,
viewId: 'my_follow',
});
loadList();
}
onBeforeMount(() => {
init();
});
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,216 @@
<template>
<MsCard auto-height simple>
<div class="flex items-center justify-between">
<div class="cursor-pointer font-medium text-[var(--color-text-1)]" @click="goTestCase">
{{ t('ms.workbench.myFollowed.feature.TEST_CASE') }}
</div>
</div>
<ms-base-table v-bind="propsRes" ref="tableRef" filter-icon-align-left class="mt-[16px]" v-on="propsEvent">
<template #num="{ record }">
<span type="text" class="one-line-text cursor-pointer px-0 text-[rgb(var(--primary-5))]">
{{ record.num }}
</span>
</template>
<template #name="{ record }">
<div class="one-line-text">{{ record.name }}</div>
</template>
<template #caseLevel="{ record }">
<span class="text-[var(--color-text-2)]">
<caseLevel :case-level="record.caseLevel" />
</span>
</template>
<!-- 用例等级 -->
<template #[FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL]="{ filterContent }">
<caseLevel :case-level="filterContent.text" />
</template>
<!-- 执行结果 -->
<template #[FilterSlotNameEnum.CASE_MANAGEMENT_EXECUTE_RESULT]="{ filterContent }">
<ExecuteStatusTag :execute-result="filterContent.value" />
</template>
<!-- 评审结果 -->
<template #reviewStatus="{ record }">
<MsIcon
:type="statusIconMap[record.reviewStatus]?.icon || ''"
class="mr-1"
:class="[statusIconMap[record.reviewStatus].color]"
></MsIcon>
<span>{{ statusIconMap[record.reviewStatus]?.statusText || '' }} </span>
</template>
<template #lastExecuteResult="{ record }">
<ExecuteStatusTag v-if="record.lastExecuteResult" :execute-result="record.lastExecuteResult" />
<span v-else>-</span>
</template>
</ms-base-table>
</MsCard>
</template>
<script setup lang="ts">
import MsCard from '@/components/pure/ms-card/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import { MsTableProps } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import ExecuteStatusTag from '@/components/business/ms-case-associate/executeResult.vue';
import { getCaseList } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import useOpenNewPage from '@/hooks/useOpenNewPage';
import { CaseManagementTable } from '@/models/caseManagement/featureCase';
import { CaseManagementRouteEnum } from '@/enums/routeEnum';
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
import {
executionResultMap,
getCaseLevels,
statusIconMap,
} from '@/views/case-management/caseManagementFeature/components/utils';
const props = defineProps<{
project: string;
}>();
const { t } = useI18n();
const { openNewPage } = useOpenNewPage();
const executeResultOptions = computed(() => {
return Object.keys(executionResultMap).map((key) => {
return {
value: key,
label: executionResultMap[key].statusText,
};
});
});
const reviewResultOptions = computed(() => {
return Object.keys(statusIconMap).map((key) => {
return {
value: key,
label: statusIconMap[key].statusText,
};
});
});
const columns: MsTableColumn = [
{
'title': 'ID',
'dataIndex': 'num',
'slotName': 'num',
'sortIndex': 1,
'fixed': 'left',
'width': 150,
'showTooltip': true,
'columnSelectorDisabled': true,
'filter-icon-align-left': true,
},
{
title: 'caseManagement.featureCase.tableColumnName',
slotName: 'name',
dataIndex: 'name',
showInTable: true,
showTooltip: true,
width: 180,
ellipsis: true,
},
{
title: 'caseManagement.featureCase.tableColumnLevel',
slotName: 'caseLevel',
dataIndex: 'caseLevel',
filterConfig: {
options: [],
filterSlotName: FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL,
},
showInTable: true,
width: 150,
showDrag: true,
},
{
title: 'caseManagement.featureCase.tableColumnReviewResult',
dataIndex: 'reviewStatus',
slotName: 'reviewStatus',
filterConfig: {
options: reviewResultOptions.value,
filterSlotName: FilterSlotNameEnum.CASE_MANAGEMENT_REVIEW_RESULT,
},
showInTable: true,
width: 150,
showDrag: true,
},
{
title: 'caseManagement.featureCase.tableColumnExecutionResult',
dataIndex: 'lastExecuteResult',
slotName: 'lastExecuteResult',
filterConfig: {
options: executeResultOptions.value,
filterSlotName: FilterSlotNameEnum.CASE_MANAGEMENT_EXECUTE_RESULT,
},
showInTable: true,
width: 150,
showDrag: true,
},
{
title: 'caseManagement.featureCase.tableColumnCreateUser',
slotName: 'createUserName',
dataIndex: 'createUserName',
showInTable: true,
showTooltip: true,
width: 200,
},
{
title: 'caseManagement.featureCase.tableColumnCreateTime',
slotName: 'createTime',
dataIndex: 'createTime',
showInTable: true,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 200,
showDrag: true,
},
];
const tableProps = ref<Partial<MsTableProps<CaseManagementTable>>>({
columns,
selectable: false,
showSetting: false,
paginationSize: 'mini',
});
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(getCaseList, tableProps.value, (record) => {
return {
...record,
tags: (record.tags || []).map((item: string, i: number) => {
return {
id: `${record.id}-${i}`,
name: item,
};
}),
visible: false,
showModuleTree: false,
caseLevel: getCaseLevels(record.customFields),
};
});
function init() {
setLoadListParams({
projectId: props.project,
viewId: 'my_follow',
});
loadList();
}
onBeforeMount(() => {
init();
});
function goTestCase() {
openNewPage(CaseManagementRouteEnum.CASE_MANAGEMENT_CASE, {
showType: 'list',
view: 'my_follow',
});
}
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,300 @@
<template>
<MsCard auto-height simple>
<div class="flex items-center justify-between">
<div class="cursor-pointer font-medium text-[var(--color-text-1)]" @click="goTestPlan">
{{ t('ms.workbench.myFollowed.feature.TEST_PLAN') }}
</div>
<a-radio-group v-model="showType" type="button" class="file-show-type mr-2" @change="fetchData">
<a-radio :value="testPlanTypeEnum.ALL" class="show-type-icon p-[2px]">
{{ t('testPlan.testPlanIndex.all') }}
</a-radio>
<a-radio :value="testPlanTypeEnum.TEST_PLAN" class="show-type-icon p-[2px]">
{{ t('testPlan.testPlanIndex.plan') }}
</a-radio>
<a-radio :value="testPlanTypeEnum.GROUP" class="show-type-icon p-[2px]">
{{ t('testPlan.testPlanIndex.testPlanGroup') }}
</a-radio>
</a-radio-group>
</div>
<MsBaseTable
v-bind="propsRes"
ref="tableRef"
class="mt-4"
filter-icon-align-left
:expanded-keys="expandedKeys"
:first-column-width="32"
v-on="propsEvent"
>
<template #num="{ record }">
<div class="flex items-center">
<PlanExpandRow
v-model:expanded-keys="expandedKeys"
:record="record"
@action="openDetail(record.id)"
@expand="expandHandler(record)"
/>
</div>
</template>
<template #[FilterSlotNameEnum.TEST_PLAN_STATUS_FILTER]="{ filterContent }">
<MsStatusTag :status="filterContent.value" />
</template>
<template #status="{ record }">
<MsStatusTag v-if="getStatus(record.id)" :status="getStatus(record.id)" />
<span v-else>-</span>
</template>
<template #createTime="{ record }">
<a-tooltip :content="`${dayjs(record.createTime).format('YYYY-MM-DD HH:mm:ss')}`" position="tl">
<div class="one-line-text">{{ dayjs(record.createTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
</a-tooltip>
</template>
<template #passRate="{ record }">
<div class="mr-[8px] w-[100px]">
<StatusProgress :status-detail="defaultCountDetailMap[record.id]" height="5px" :type="record.type" />
</div>
<div class="text-[var(--color-text-1)]">
{{ `${defaultCountDetailMap[record.id]?.passRate ? defaultCountDetailMap[record.id].passRate : '-'}%` }}
</div>
</template>
<template #passRateTitleSlot="{ columnConfig }">
<div class="flex items-center text-[var(--color-text-3)]">
{{ t(columnConfig.title as string) }}
<a-tooltip position="right" :content="t('testPlan.testPlanIndex.passRateTitleTip')">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</div>
</template>
<template #functionalCaseCount="{ record }">
<a-popover position="bottom" content-class="p-[16px]" :disabled="getFunctionalCount(record.id) < 1">
<div>{{ getFunctionalCount(record.id) }}</div>
<template #content>
<table class="min-w-[140px] max-w-[176px]">
<tr>
<td class="popover-label-td">
<div>{{ t('testPlan.testPlanIndex.TotalCases') }}</div>
</td>
<td class="popover-value-td">
{{ defaultCountDetailMap[record.id]?.caseTotal ?? '0' }}
</td>
</tr>
<tr>
<td class="popover-label-td">
<div class="text-[var(--color-text-1)]">{{ t('testPlan.testPlanIndex.functionalUseCase') }}</div>
</td>
<td class="popover-value-td">
{{ defaultCountDetailMap[record.id]?.functionalCaseCount ?? '0' }}
</td>
</tr>
<tr>
<td class="popover-label-td">
<div class="text-[var(--color-text-1)]">{{ t('testPlan.testPlanIndex.apiCase') }}</div>
</td>
<td class="popover-value-td">
{{ defaultCountDetailMap[record.id]?.apiCaseCount ?? '0' }}
</td>
</tr>
<tr>
<td class="popover-label-td">
<div class="text-[var(--color-text-1)]">{{ t('testPlan.testPlanIndex.apiScenarioCase') }}</div>
</td>
<td class="popover-value-td">
{{ defaultCountDetailMap[record.id]?.apiScenarioCount ?? '0' }}
</td>
</tr>
</table>
</template>
</a-popover>
</template>
</MsBaseTable>
</MsCard>
</template>
<script setup lang="ts">
import dayjs from 'dayjs';
import MsCard from '@/components/pure/ms-card/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { MsTableColumn, MsTableProps } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsStatusTag from '@/components/business/ms-status-tag/index.vue';
import PlanExpandRow from '@/views/test-plan/testPlan/components/planExpandRow.vue';
import StatusProgress from '@/views/test-plan/testPlan/components/statusProgress.vue';
import { getPlanPassRate, getTestPlanList } from '@/api/modules/test-plan/testPlan';
import { useI18n } from '@/hooks/useI18n';
import useOpenNewPage from '@/hooks/useOpenNewPage';
import { PassRateCountDetail, TestPlanItem } from '@/models/testPlan/testPlan';
import { TestPlanRouteEnum } from '@/enums/routeEnum';
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
import { testPlanTypeEnum } from '@/enums/testPlanEnum';
import { planStatusOptions } from '@/views/test-plan/testPlan/config';
const props = defineProps<{
project: string;
}>();
const { t } = useI18n();
const { openNewPage } = useOpenNewPage();
const showType = ref(testPlanTypeEnum.ALL);
const defaultCountDetailMap = ref<Record<string, PassRateCountDetail>>({});
function getFunctionalCount(id: string) {
return defaultCountDetailMap.value[id]?.caseTotal ?? 0;
}
function getStatus(id: string) {
return defaultCountDetailMap.value[id]?.status;
}
async function getStatistics(selectedPlanIds: (string | undefined)[]) {
try {
const result = await getPlanPassRate(selectedPlanIds);
result.forEach((item: PassRateCountDetail) => {
defaultCountDetailMap.value[item.id] = item;
});
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
const columns: MsTableColumn = [
{
title: 'testPlan.testPlanIndex.ID',
slotName: 'num',
dataIndex: 'num',
width: 180,
showInTable: true,
showDrag: false,
},
{
title: 'testPlan.testPlanIndex.testPlanName',
slotName: 'name',
dataIndex: 'name',
showInTable: true,
showTooltip: true,
width: 180,
showDrag: false,
},
{
title: 'common.status',
dataIndex: 'status',
slotName: 'status',
filterConfig: {
options: planStatusOptions,
filterSlotName: FilterSlotNameEnum.TEST_PLAN_STATUS_FILTER,
},
showInTable: true,
showDrag: true,
width: 150,
},
{
title: 'testPlan.testPlanIndex.passRate',
dataIndex: 'passRate',
slotName: 'passRate',
titleSlotName: 'passRateTitleSlot',
showInTable: true,
width: 200,
showDrag: true,
},
{
title: 'testPlan.testPlanIndex.useCount',
slotName: 'functionalCaseCount',
dataIndex: 'functionalCaseCount',
showInTable: true,
width: 150,
showDrag: true,
},
{
title: 'testPlan.testPlanIndex.createTime',
slotName: 'createTime',
dataIndex: 'createTime',
showInTable: true,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 200,
showDrag: true,
showTooltip: true,
},
];
const tableProps = ref<Partial<MsTableProps<TestPlanItem>>>({
columns,
selectable: false,
showSetting: false,
paginationSize: 'mini',
showSelectorAll: false,
});
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(getTestPlanList, tableProps.value);
const planData = computed(() => {
return propsRes.value.data;
});
const expandedKeys = ref<string[]>([]);
//
function expandHandler(record: TestPlanItem) {
if (expandedKeys.value.includes(record.id)) {
expandedKeys.value = expandedKeys.value.filter((key) => key !== record.id);
} else {
expandedKeys.value = [...expandedKeys.value, record.id];
if (record.type === 'GROUP' && record.childrenCount) {
const testPlanId = record.children.map((item: TestPlanItem) => item.id);
getStatistics(testPlanId);
}
}
}
//
function openDetail(id: string) {
openNewPage(TestPlanRouteEnum.TEST_PLAN_INDEX_DETAIL, {
id,
});
}
watch(
() => planData.value,
(val) => {
if (val) {
const selectedPlanIds: (string | undefined)[] = propsRes.value.data.map((e) => e.id) || [];
if (selectedPlanIds.length) {
getStatistics(selectedPlanIds);
}
}
},
{
immediate: true,
}
);
function fetchData() {
setLoadListParams({
type: showType.value,
projectId: props.project,
viewId: 'my_follow',
});
loadList();
}
function goTestPlan() {
openNewPage(TestPlanRouteEnum.TEST_PLAN_INDEX, {
showType: showType.value,
view: 'my_follow',
});
}
onBeforeMount(() => {
fetchData();
});
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,80 @@
<template>
<div class="flex flex-col gap-[16px]">
<div class="flex items-center justify-end gap-[12px]">
<MsProjectSelect v-model:project="currentProject" class="w-[240px]" use-default-arrow-icon>
<template #prefix>
{{ t('menu.projectManagementShort') }}
</template>
</MsProjectSelect>
<a-select
v-model:model-value="features"
:options="featureOptions"
:max-tag-count="1"
multiple
class="w-[240px]"
@change="handleFeatureChange"
>
<template #prefix>
{{ t('project.messageManagement.function') }}
</template>
<template #header>
<a-checkbox v-model:model-value="featureAll" class="ml-[8px]" @change="handleFeatureAllChange">
{{ t('common.all') }}
</a-checkbox>
</template>
</a-select>
<a-button type="outline" class="arco-btn-outline--secondary p-[10px]" @click="handleRefresh">
<MsIcon type="icon-icon_reset_outlined" size="14" />
</a-button>
</div>
<testPlanTable v-if="features.includes(FeatureEnum.TEST_PLAN)" :project="currentProject" />
<testCaseTable v-if="features.includes(FeatureEnum.TEST_CASE)" :project="currentProject" />
<caseReviewTable v-if="features.includes(FeatureEnum.CASE_REVIEW)" :project="currentProject" />
<apiCaseTable v-if="features.includes(FeatureEnum.API_CASE)" :project="currentProject" />
<scenarioCaseTable v-if="features.includes(FeatureEnum.API_SCENARIO)" :project="currentProject" />
<bugTable v-if="features.includes(FeatureEnum.BUG)" :project="currentProject" />
</div>
</template>
<script setup lang="ts">
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsProjectSelect from '@/components/business/ms-project-select/index.vue';
import apiCaseTable from './components/apiCaseTable.vue';
import bugTable from './components/bugTable.vue';
import caseReviewTable from './components/caseReviewTable.vue';
import scenarioCaseTable from './components/scenarioCaseTable.vue';
import testCaseTable from './components/testCaseTable.vue';
import testPlanTable from './components/testPlanTable.vue';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { FeatureEnum } from '@/enums/workbenchEnum';
const { t } = useI18n();
const appStore = useAppStore();
const currentProject = ref(appStore.currentProjectId);
const features = ref<FeatureEnum[]>(Object.values(FeatureEnum));
const featureOptions = Object.keys(FeatureEnum).map((key) => ({
label: t(`ms.workbench.myFollowed.feature.${key}`),
value: key as FeatureEnum,
}));
const featureAll = ref(true);
function handleFeatureAllChange(val: boolean | (string | number | boolean)[]) {
features.value = val ? featureOptions.map((item) => item.value) : [];
}
function handleFeatureChange(
val: string | number | boolean | Record<string, any> | (string | number | boolean | Record<string, any>)[]
) {
featureAll.value = (val as []).length === featureOptions.length;
}
function handleRefresh() {
console.log('refresh');
}
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,8 @@
export default {
'ms.workbench.myFollowed.feature.TEST_PLAN': 'Test Plan',
'ms.workbench.myFollowed.feature.TEST_CASE': 'Test Case',
'ms.workbench.myFollowed.feature.CASE_REVIEW': 'Case Review',
'ms.workbench.myFollowed.feature.API_CASE': 'Interface Case',
'ms.workbench.myFollowed.feature.API_SCENARIO': 'Interface Scenario',
'ms.workbench.myFollowed.feature.BUG': 'Bug',
};

View File

@ -0,0 +1,8 @@
export default {
'ms.workbench.myFollowed.feature.TEST_PLAN': '测试计划',
'ms.workbench.myFollowed.feature.TEST_CASE': '测试用例',
'ms.workbench.myFollowed.feature.CASE_REVIEW': '用例评审',
'ms.workbench.myFollowed.feature.API_CASE': '接口用例',
'ms.workbench.myFollowed.feature.API_SCENARIO': '接口场景',
'ms.workbench.myFollowed.feature.BUG': '缺陷',
};