feat(接口测试): 接口调试-部分 tab&用例评审部分接口联调&部分组件调整

This commit is contained in:
baiqi 2024-01-12 14:17:17 +08:00 committed by Craftsman
parent f227f97679
commit f6fb6d384b
65 changed files with 2834 additions and 1201 deletions

View File

@ -3,31 +3,52 @@ import {
AddReviewModuleUrl, AddReviewModuleUrl,
AddReviewUrl, AddReviewUrl,
AssociateReviewUrl, AssociateReviewUrl,
BatchChangeReviewerUrl,
BatchDisassociateReviewCaseUrl,
BatchReviewUrl,
CopyReviewUrl, CopyReviewUrl,
DeleteReviewModuleUrl, DeleteReviewModuleUrl,
DeleteReviewUrl,
DisassociateReviewCaseUrl,
EditReviewUrl, EditReviewUrl,
FollowReviewUrl, FollowReviewUrl,
GetAssociatedIdsUrl, GetAssociatedIdsUrl,
GetCaseReviewHistoryListUrl,
GetReviewDetailCasePageUrl,
GetReviewDetailModuleCountUrl,
GetReviewDetailModuleTreeUrl,
GetReviewDetailUrl, GetReviewDetailUrl,
GetReviewListUrl, GetReviewListUrl,
GetReviewModulesUrl, GetReviewModulesUrl,
GetReviewUsersUrl, GetReviewUsersUrl,
MoveReviewModuleUrl, MoveReviewModuleUrl,
MoveReviewUrl, MoveReviewUrl,
ReviewModuleCountUrl,
SaveCaseReviewResultUrl,
SortReviewDetailCaseUrl,
SortReviewUrl, SortReviewUrl,
UpdateReviewModuleUrl, UpdateReviewModuleUrl,
} from '@/api/requrls/case-management/caseReview'; } from '@/api/requrls/case-management/caseReview';
import { import {
AssociateReviewCaseParams, AssociateReviewCaseParams,
BatchCancelReviewCaseParams,
BatchChangeReviewerParams,
BatchMoveReviewParams, BatchMoveReviewParams,
BatchReviewCaseParams,
CommitReviewResultParams,
CopyReviewParams,
FollowReviewParams, FollowReviewParams,
Review, Review,
ReviewCaseItem,
ReviewDetailCaseListQueryParams,
ReviewHistoryItem,
ReviewItem, ReviewItem,
ReviewListQueryParams, ReviewListQueryParams,
ReviewModule, ReviewModule,
ReviewModuleItem, ReviewModuleItem,
ReviewUserItem, ReviewUserItem,
SortReviewCaseParams,
SortReviewParams, SortReviewParams,
UpdateReviewModuleParams, UpdateReviewModuleParams,
UpdateReviewParams, UpdateReviewParams,
@ -59,6 +80,11 @@ export const deleteReviewModule = (id: string) => {
return MSR.get({ url: DeleteReviewModuleUrl, params: id }); return MSR.get({ url: DeleteReviewModuleUrl, params: id });
}; };
// 评审模块树-统计用例数量
export const reviewModuleCount = (data: ReviewListQueryParams) => {
return MSR.post({ url: ReviewModuleCountUrl, data });
};
// 新增评审 // 新增评审
export const addReview = (data: Review) => { export const addReview = (data: Review) => {
return MSR.post({ url: AddReviewUrl, data }); return MSR.post({ url: AddReviewUrl, data });
@ -70,7 +96,7 @@ export const associateReviewCase = (data: AssociateReviewCaseParams) => {
}; };
// 复制评审 // 复制评审
export const copyReview = (data: Review) => { export const copyReview = (data: CopyReviewParams) => {
return MSR.post({ url: CopyReviewUrl, data }); return MSR.post({ url: CopyReviewUrl, data });
}; };
@ -109,7 +135,62 @@ export const getReviewUsers = (projectId: string, keyword: string) => {
return MSR.get<ReviewUserItem[]>({ url: `${GetReviewUsersUrl}/${projectId}`, params: { keyword } }); return MSR.get<ReviewUserItem[]>({ url: `${GetReviewUsersUrl}/${projectId}`, params: { keyword } });
}; };
// 获取评审人员列表 // 取消关联用例
export const disassociateReviewCase = (reviewId: string, caseId: string) => {
return MSR.get<ReviewUserItem[]>({ url: `${DisassociateReviewCaseUrl}/${reviewId}/${caseId}` });
};
// 删除评审
export const deleteReview = (reviewId: string, projectId: string) => {
return MSR.get<ReviewUserItem[]>({ url: `${DeleteReviewUrl}/${projectId}/${reviewId}` });
};
// 评审详情-获取用例列表
export const getReviewDetailCasePage = (data: ReviewDetailCaseListQueryParams) => {
return MSR.post<CommonList<ReviewCaseItem>>({ url: GetReviewDetailCasePageUrl, data });
};
// 评审详情-用例拖拽排序
export const sortReviewDetailCase = (data: SortReviewCaseParams) => {
return MSR.post({ url: SortReviewDetailCaseUrl, data });
};
// 评审详情-批量评审
export const batchReview = (data: BatchReviewCaseParams) => {
return MSR.post({ url: BatchReviewUrl, data });
};
// 评审详情-批量修改评审人
export const batchChangeReviewer = (data: BatchChangeReviewerParams) => {
return MSR.post({ url: BatchChangeReviewerUrl, data });
};
// 评审详情-批量取消关联用例
export const batchDisassociateReviewCase = (data: BatchCancelReviewCaseParams) => {
return MSR.post({ url: BatchDisassociateReviewCaseUrl, data });
};
// 获取关联用例 id集合
export const getAssociatedIds = (reviewId: string) => { export const getAssociatedIds = (reviewId: string) => {
return MSR.get<string[]>({ url: `${GetAssociatedIdsUrl}/${reviewId}` }); return MSR.get<string[]>({ url: `${GetAssociatedIdsUrl}/${reviewId}` });
}; };
// 评审详情-模块下用例数量统计
export const getReviewDetailModuleCount = (data: ReviewDetailCaseListQueryParams) => {
return MSR.post({ url: GetReviewDetailModuleCountUrl, data });
};
// 评审详情-已关联用例模块树
export const getReviewDetailModuleTree = (projectId: string, reviewId: string) => {
return MSR.get({ url: `${GetReviewDetailModuleTreeUrl}/${projectId}/${reviewId}` });
};
// 评审详情-获取用例评审历史
export const getCaseReviewHistoryList = (reviewId: string, caseId: string) => {
return MSR.get<ReviewHistoryItem[]>({ url: `${GetCaseReviewHistoryListUrl}/${reviewId}/${caseId}` });
};
// 评审详情-提交用例评审结果
export const saveCaseReviewResult = (data: CommitReviewResultParams) => {
return MSR.post({ url: SaveCaseReviewResultUrl, data });
};

View File

@ -76,6 +76,7 @@ import type {
CreateOrUpdateModule, CreateOrUpdateModule,
DeleteCaseType, DeleteCaseType,
DemandItem, DemandItem,
DetailCase,
ModulesTreeType, ModulesTreeType,
OperationFile, OperationFile,
UpdateModule, UpdateModule,

View File

@ -11,15 +11,19 @@ import {
GetInfoUrl, GetInfoUrl,
GetLocalConfigUrl, GetLocalConfigUrl,
GetMenuListUrl, GetMenuListUrl,
GetPlatformAccountUrl,
GetPlatformUrl,
GetPublicKeyUrl, GetPublicKeyUrl,
isLoginUrl, isLoginUrl,
LoginUrl, LoginUrl,
LogoutUrl, LogoutUrl,
SavePlatformUrl,
UpdateAPIKEYUrl, UpdateAPIKEYUrl,
UpdateInfoUrl, UpdateInfoUrl,
UpdateLocalConfigUrl, UpdateLocalConfigUrl,
UpdatePswUrl, UpdatePswUrl,
ValidAPIKEYUrl, ValidAPIKEYUrl,
ValidatePlatformUrl,
ValidLocalConfigUrl, ValidLocalConfigUrl,
} from '@/api/requrls/user'; } from '@/api/requrls/user';
@ -137,3 +141,23 @@ export function updateBaseInfo(data: UpdateBaseInfo) {
export function updatePsw(data: UpdatePswParams) { export function updatePsw(data: UpdatePswParams) {
return MSR.post({ url: UpdatePswUrl, data }); return MSR.post({ url: UpdatePswUrl, data });
} }
// 个人信息-校验第三方平台账号信息
export function validatePlatform(id: string, data: Record<string, any>) {
return MSR.post({ url: `${ValidatePlatformUrl}/${id}`, data });
}
// 个人信息-保存第三方平台账号信息
export function savePlatform(data: UpdatePswParams) {
return MSR.post({ url: SavePlatformUrl, data });
}
// 个人信息-获取第三方平台账号信息
export function getPlatform() {
return MSR.get({ url: GetPlatformUrl });
}
// 个人信息-获取第三方平台账号信息-插件信息
export function getPlatformAccount() {
return MSR.get({ url: GetPlatformAccountUrl });
}

View File

@ -8,9 +8,21 @@ export const AssociateReviewUrl = '/case/review/associate'; // 关联用例
export const AddReviewUrl = '/case/review/add'; // 新增评审 export const AddReviewUrl = '/case/review/add'; // 新增评审
export const GetReviewUsersUrl = '/case/review/user-option'; // 获取评审人员列表 export const GetReviewUsersUrl = '/case/review/user-option'; // 获取评审人员列表
export const GetReviewDetailUrl = '/case/review/detail'; // 获取评审详情 export const GetReviewDetailUrl = '/case/review/detail'; // 获取评审详情
export const DisassociateReviewCaseUrl = '/case/review/disassociate'; // 取消关联用例
export const DeleteReviewUrl = '/case/review/delete'; // 删除用例评审
export const UpdateReviewModuleUrl = '/case/review/module/update'; // 更新评审模块 export const UpdateReviewModuleUrl = '/case/review/module/update'; // 更新评审模块
export const MoveReviewModuleUrl = '/case/review/module/move'; // 移动评审模块 export const MoveReviewModuleUrl = '/case/review/module/move'; // 移动评审模块
export const AddReviewModuleUrl = '/case/review/module/add'; // 新增评审模块 export const AddReviewModuleUrl = '/case/review/module/add'; // 新增评审模块
export const GetReviewModulesUrl = '/case/review/module/tree'; // 获取评审模块树 export const GetReviewModulesUrl = '/case/review/module/tree'; // 获取评审模块树
export const DeleteReviewModuleUrl = '/case/review/module/delete'; // 删除评审模块 export const DeleteReviewModuleUrl = '/case/review/module/delete'; // 删除评审模块
export const ReviewModuleCountUrl = '/case/review/module/count'; // 模块下用例数量统计
export const GetReviewDetailCasePageUrl = '/case/review/detail/page'; // 评审详情-获取已关联用例列表
export const SortReviewDetailCaseUrl = '/case/review/detail/edit/pos'; // 评审详情-已关联用例拖拽排序
export const BatchReviewUrl = '/case/review/detail/batch/review'; // 评审详情-批量评审
export const BatchChangeReviewerUrl = '/case/review/detail/batch/edit/reviewers'; // 评审详情-批量修改评审人
export const BatchDisassociateReviewCaseUrl = '/case/review/detail/batch/disassociate'; // 评审详情-批量取消关联用例
export const GetAssociatedIdsUrl = '/case/review/detail/get-ids'; // 获取已关联用例id集合 export const GetAssociatedIdsUrl = '/case/review/detail/get-ids'; // 获取已关联用例id集合
export const GetReviewDetailModuleCountUrl = '/case/review/detail/module/count'; // 评审详情-模块下用例数量统计
export const GetReviewDetailModuleTreeUrl = '/case/review/detail/tree'; // 评审详情-已关联用例模块树
export const GetCaseReviewHistoryListUrl = '/review/functional/case/get/list'; // 评审详情-获取用例评审历史
export const SaveCaseReviewResultUrl = '/review/functional/case/save'; // 评审详情-提交评审

View File

@ -19,3 +19,7 @@ export const AddAPIKEYUrl = '/user/api/key/add'; // 个人设置-生成 APIKEY
export const UpdatePswUrl = '/personal/update-password'; // 个人信息-修改密码 export const UpdatePswUrl = '/personal/update-password'; // 个人信息-修改密码
export const UpdateInfoUrl = '/personal/update-info'; // 个人信息-修改信息 export const UpdateInfoUrl = '/personal/update-info'; // 个人信息-修改信息
export const GetInfoUrl = '/personal/get'; // 个人信息-获取信息 export const GetInfoUrl = '/personal/get'; // 个人信息-获取信息
export const ValidatePlatformUrl = '/user/platform/validate'; // 个人信息-校验服务集成信息
export const SavePlatformUrl = '/user/platform/save'; // 个人信息-保存三方平台账号信息
export const GetPlatformUrl = '/user/platform/get'; // 个人信息-获取三方平台账号信息
export const GetPlatformAccountUrl = '/user/platform/account/info'; // 个人信息-获取三方平台账号信息-插件信息

View File

@ -407,7 +407,7 @@
} }
} }
} }
.arco-radio-checked { .arco-radio-checked:not(.arco-radio-disabled) {
.arco-radio-icon { .arco-radio-icon {
@apply !bg-white; @apply !bg-white;
@ -425,6 +425,15 @@
} }
} }
} }
.arco-radio-checked.arco-radio-disabled {
.arco-radio-icon {
border: 1px solid var(--color-text-input-border);
background-color: var(--color-text-n8) !important;
&::after {
background-color: var(--color-text-4) !important;
}
}
}
/** Message **/ /** Message **/
.arco-message { .arco-message {

View File

@ -15,9 +15,9 @@
class="ml-2 max-w-[100px]" class="ml-2 max-w-[100px]"
:placeholder="t('caseManagement.featureCase.PleaseSelect')" :placeholder="t('caseManagement.featureCase.PleaseSelect')"
> >
<a-option v-for="item of props?.moduleOptions" :key="item.value" :value="item.value">{{ <a-option v-for="item of props?.moduleOptions" :key="item.value" :value="item.value">
t(item.label) {{ t(item.label) }}
}}</a-option> </a-option>
</a-select> </a-select>
</div> </div>
</template> </template>
@ -142,7 +142,7 @@
import type { MsTreeNodeData } from '@/components/business/ms-tree/types'; import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import caseLevel from './caseLevel.vue'; import caseLevel from './caseLevel.vue';
import { getCustomFieldsTable } from '@/api/modules/case-management/featureCase'; import { getCaseModulesCounts, getCustomFieldsTable } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { mapTree } from '@/utils'; import { mapTree } from '@/utils';
@ -164,7 +164,6 @@
modulesParams?: Record<string, any>; // modulesParams?: Record<string, any>; //
getTableFunc: (params: TableQueryParams) => Promise<CommonList<CaseManagementTable>>; // getTableFunc: (params: TableQueryParams) => Promise<CommonList<CaseManagementTable>>; //
tableParams?: TableQueryParams; // tableParams?: TableQueryParams; //
modulesCount: Record<string, any>; //
okButtonDisabled?: boolean; // okButtonDisabled?: boolean; //
currentSelectCase: string | number | Record<string, any> | undefined; // currentSelectCase: string | number | Record<string, any> | undefined; //
moduleOptions?: { label: string; value: string }[]; // moduleOptions?: { label: string; value: string }[]; //
@ -178,7 +177,7 @@
(e: 'update:currentSelectCase', val: string | number | Record<string, any> | undefined): void; (e: 'update:currentSelectCase', val: string | number | Record<string, any> | undefined): void;
(e: 'init', val: TableQueryParams): void; // (e: 'init', val: TableQueryParams): void; //
(e: 'close'): void; (e: 'close'): void;
(e: 'save', params: TableQueryParams): void; // table (e: 'save', params: any): void; // table
}>(); }>();
const virtualListProps = computed(() => { const virtualListProps = computed(() => {
@ -212,6 +211,7 @@
const protocolType = ref('HTTP'); // const protocolType = ref('HTTP'); //
const protocolOptions = ref(['HTTP']); const protocolOptions = ref(['HTTP']);
const modulesCount = ref<Record<string, any>>({});
// //
const caseType = computed({ const caseType = computed({
@ -248,7 +248,7 @@
hideMoreAction: e.id === 'root', hideMoreAction: e.id === 'root',
draggable: false, draggable: false,
disabled: false, disabled: false,
count: props.modulesCount?.[e.id] || 0, count: modulesCount.value[e.id] || 0,
}; };
}); });
if (isSetDefaultKey) { if (isSetDefaultKey) {
@ -498,16 +498,22 @@
} }
// //
function initModuleCount() { async function initModuleCount() {
emit('init', { try {
keyword: keyword.value, const params = {
moduleIds: [], keyword: keyword.value,
projectId: innerProject.value, moduleIds: selectedModuleKeys.value,
current: propsRes.value.msPagination?.current, projectId: innerProject.value,
pageSize: propsRes.value.msPagination?.pageSize, current: propsRes.value.msPagination?.current,
sourceId: props.caseId, pageSize: propsRes.value.msPagination?.pageSize,
combine: combine.value, combine: combine.value,
}); };
modulesCount.value = await getCaseModulesCounts(params);
emit('init', params);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
} }
function searchCase() { function searchCase() {
@ -556,6 +562,7 @@
innerVisible.value = val; innerVisible.value = val;
if (val) { if (val) {
resetSelector(); resetSelector();
initModules();
searchCase(); searchCase();
initFilter(); initFilter();
} }
@ -567,7 +574,7 @@
(val) => { (val) => {
emit('update:visible', val); emit('update:visible', val);
if (val) { if (val) {
initModules(true); initModules();
} }
} }
); );
@ -578,7 +585,7 @@
(val) => { (val) => {
if (val) { if (val) {
emit('update:currentSelectCase', val); emit('update:currentSelectCase', val);
initModules(true); initModules();
searchCase(); searchCase();
} }
} }
@ -597,8 +604,12 @@
() => innerProject.value, () => innerProject.value,
(val) => { (val) => {
emit('update:project', val); emit('update:project', val);
initModules(true); if (innerVisible.value) {
searchCase(); searchCase();
resetSelector();
initModules();
searchCase();
}
} }
); );
@ -613,7 +624,7 @@
* 初始化模块数量 * 初始化模块数量
*/ */
watch( watch(
() => props.modulesCount, () => modulesCount.value,
(obj) => { (obj) => {
folderTree.value = mapTree<ModuleTreeNode>(folderTree.value, (node) => { folderTree.value = mapTree<ModuleTreeNode>(folderTree.value, (node) => {
return { return {

View File

@ -22,6 +22,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { useVModel } from '@vueuse/core';
import MsAvatar from '@/components/pure/ms-avatar/index.vue'; import MsAvatar from '@/components/pure/ms-avatar/index.vue';
import MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue'; import MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue';
@ -37,17 +38,12 @@
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(event: 'update:content', value: string): void;
(event: 'publish', value: string): void; (event: 'publish', value: string): void;
}>(); }>();
const isActive = ref(false); const isActive = ref(false);
const currentContent = ref(''); const currentContent = useVModel(props, 'content', emit);
watchEffect(() => {
if (props.content) {
currentContent.value = props.content;
}
});
const publish = () => { const publish = () => {
emit('publish', currentContent.value); emit('publish', currentContent.value);

View File

@ -4,163 +4,42 @@
<div class="font-medium text-[var(--color-text-1)]">{{ t('ms.personal.tripartite') }}</div> <div class="font-medium text-[var(--color-text-1)]">{{ t('ms.personal.tripartite') }}</div>
</div> </div>
<div class="platform-card-container"> <div class="platform-card-container">
<div class="platform-card"> <div v-for="config of dynamicForm" :key="config.key" class="platform-card">
<div class="mb-[16px] flex items-center"> <div class="mb-[16px] flex items-center">
<a-image src="/plugin/image/jira?imagePath=static/jira.jpg" width="24"></a-image> <a-image :src="`/plugin/image/${config.key}?imagePath=static/${config.key}.jpg`" width="24"></a-image>
<div class="ml-[8px] mr-[4px] font-medium text-[var(--color-text-1)]">JIRA</div> <div class="ml-[8px] mr-[4px] font-medium text-[var(--color-text-1)]">{{ config.key }}</div>
<a-tooltip :content="t('ms.personal.jiraTip')" position="right"> <a-tooltip v-if="config.tooltip" :content="config.tooltip" position="right">
<icon-exclamation-circle <icon-exclamation-circle
class="mr-[8px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]" class="mr-[8px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16" size="16"
/> />
</a-tooltip> </a-tooltip>
<MsTag theme="light" :type="tagMap[jiraConfig.status].type" size="small" class="px-[4px]"> <MsTag theme="light" :type="tagMap[config.status].type" size="small" class="px-[4px]">
{{ tagMap[jiraConfig.status].text }} {{ tagMap[config.status].text }}
</MsTag> </MsTag>
</div> </div>
<a-form ref="jiraFormRef" :model="jiraConfig"> <MsFormCreate
<a-form-item :label="t('ms.personal.authType')"> v-model:api="config.formModel"
<a-radio-group v-model:model-value="jiraConfig.authType"> v-model:form-item="config.formItemList"
<a-radio value="basic">Basic Auth</a-radio> :form-rule="config.formRules"
<a-radio value="token">Bearer Token</a-radio> :option="options"
</a-radio-group> >
</a-form-item> </MsFormCreate>
<a-form-item :label="t('ms.personal.platformAccount')"> <a-button type="outline" :loading="config.validateLoading" @click="validate(config)">
<a-input {{ t('ms.personal.valid') }}
v-model:model-value="jiraConfig.platformAccount" </a-button>
:placeholder="t('ms.personal.platformAccountPlaceholder', { type: 'JIRA' })"
class="w-[312px]"
allow-clear
autocomplete="new-password"
></a-input>
</a-form-item>
<a-form-item :label="t('ms.personal.platformPsw')">
<a-input-password
v-model:model-value="jiraConfig.platformPsw"
:placeholder="t('ms.personal.platformPswPlaceholder', { type: 'JIRA' })"
class="mr-[8px] w-[312px]"
allow-clear
autocomplete="new-password"
></a-input-password>
<a-button type="outline" :disabled="jiraConfig.platformAccount === '' || jiraConfig.platformPsw === ''">
{{ t('ms.personal.valid') }}
</a-button>
</a-form-item>
</a-form>
</div>
<div class="platform-card">
<div class="mb-[16px] flex items-center">
<a-image src="/plugin/image/jira?imagePath=static/jira.jpg" width="24"></a-image>
<div class="ml-[8px] mr-[4px] font-medium text-[var(--color-text-1)]">
{{ t('ms.personal.zendao') }}
</div>
<a-tooltip :content="t('ms.personal.zendaoTip')" position="right">
<icon-exclamation-circle
class="mr-[8px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
<MsTag theme="light" :type="tagMap[zendaoConfig.status].type" size="small" class="px-[4px]">
{{ tagMap[zendaoConfig.status].text }}
</MsTag>
</div>
<a-form ref="zendaoFormRef" :model="zendaoConfig">
<a-form-item :label="t('ms.personal.platformAccount')">
<a-input
v-model:model-value="zendaoConfig.platformAccount"
:placeholder="t('ms.personal.platformAccountPlaceholder', { type: t('ms.personal.zendao') })"
class="w-[312px]"
allow-clear
autocomplete="new-password"
></a-input>
</a-form-item>
<a-form-item :label="t('ms.personal.platformPsw')">
<a-input-password
v-model:model-value="zendaoConfig.platformPsw"
:placeholder="t('ms.personal.platformPswPlaceholder', { type: t('ms.personal.zendao') })"
class="mr-[8px] w-[312px]"
allow-clear
autocomplete="new-password"
></a-input-password>
<a-button type="outline" :disabled="zendaoConfig.platformAccount === '' || zendaoConfig.platformPsw === ''">
{{ t('ms.personal.valid') }}
</a-button>
</a-form-item>
</a-form>
</div>
<div class="platform-card">
<div class="mb-[16px] flex items-center">
<a-image src="/plugin/image/jira?imagePath=static/jira.jpg" width="24"></a-image>
<div class="ml-[8px] mr-[4px] font-medium text-[var(--color-text-1)]"> Azure DeVops </div>
<a-tooltip :content="t('ms.personal.azureTip')" position="right">
<icon-exclamation-circle
class="mr-[8px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
<MsTag theme="light" :type="tagMap[azureConfig.status].type" size="small" class="px-[4px]">
{{ tagMap[azureConfig.status].text }}
</MsTag>
</div>
<a-form ref="zendaoFormRef" :model="azureConfig">
<a-form-item>
<template #label>
<div class="flex text-right leading-none"> Personal Access Tokens </div>
</template>
<a-input
v-model:model-value="azureConfig.token"
:placeholder="t('ms.personal.azurePlaceholder')"
class="mr-[8px] w-[312px]"
allow-clear
autocomplete="new-password"
></a-input>
<a-button type="outline" :disabled="azureConfig.token === ''">
{{ t('ms.personal.valid') }}
</a-button>
</a-form-item>
</a-form>
</div>
<div class="platform-card">
<div class="mb-[16px] flex items-center">
<a-image src="/plugin/image/jira?imagePath=static/jira.jpg" width="24"></a-image>
<div class="ml-[8px] mr-[4px] font-medium text-[var(--color-text-1)]"> TAPD </div>
<a-popover position="right">
<template #content>
<div class="bg-[var(--color-text-n9)] p-[12px]">
<a-image src="/images/tapd-user.png" :width="385"></a-image>
</div>
</template>
<icon-exclamation-circle
class="mr-[8px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-popover>
<MsTag theme="light" :type="tagMap[tapdConfig.status].type" size="small" class="px-[4px]">
{{ tagMap[tapdConfig.status].text }}
</MsTag>
</div>
<a-form ref="zendaoFormRef" :model="tapdConfig">
<a-form-item :label="t('ms.personal.platformName')">
<a-input
v-model:model-value="tapdConfig.name"
:placeholder="t('ms.personal.platformNamePlaceholder')"
class="mr-[8px] w-[312px]"
allow-clear
autocomplete="new-password"
/>
<a-button type="outline" :disabled="tapdConfig.name === ''">
{{ t('ms.personal.valid') }}
</a-button>
</a-form-item>
</a-form>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Message } from '@arco-design/web-vue';
import MsFormCreate from '@/components/pure/ms-form-create/ms-form-create.vue';
import MsTag, { TagType } from '@/components/pure/ms-tag/ms-tag.vue'; import MsTag, { TagType } from '@/components/pure/ms-tag/ms-tag.vue';
import { getPlatform, getPlatformAccount, savePlatform, validatePlatform } from '@/api/modules/user/index';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n(); const { t } = useI18n();
@ -185,33 +64,75 @@
}, },
}; };
const jiraConfig = ref({ const dynamicForm = ref<any>({});
status: 0 as Status, const options = ref({
authType: 'basic', resetBtn: false,
platformAccount: '', submitBtn: false,
platformPsw: '', on: false,
form: {
layout: 'vertical',
labelAlign: 'left',
},
row: {
gutter: 0,
},
wrap: {
'asterisk-position': 'end',
'validate-trigger': ['change'],
},
}); });
const zendaoConfig = ref({ async function initPlatformAccountInfo() {
status: 0 as Status, try {
platformAccount: '', const res = await getPlatformAccount();
platformPsw: '', Object.keys(res).forEach((key) => {
}); dynamicForm.value[key] = {
key,
status: 0,
formModel: {},
formRules: res[key].formItems,
formItemList: [],
tooltip: res[key].instructionsInfo,
validateLoading: false,
};
});
} catch (error) {
console.log(error);
}
}
const azureConfig = ref({ async function initPlatformInfo() {
status: 0 as Status, try {
token: '', const res = await getPlatform();
}); } catch (error) {
console.log(error);
}
}
const tapdConfig = ref({ async function validate(config: any) {
status: 0 as Status, try {
name: '', config.validateLoading = true;
await validatePlatform(config.key, config.formModel.form);
Message.success(t('ms.personal.validPass'));
config.status = 1;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
config.status = 2;
} finally {
config.validateLoading = false;
}
}
onBeforeMount(() => {
initPlatformAccountInfo();
initPlatformInfo();
}); });
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.platform-card-container { .platform-card-container {
@apply flex flex-1 flex-wrap overflow-auto; @apply flex flex-wrap overflow-auto;
.ms-scroll-bar(); .ms-scroll-bar();
padding: 16px; padding: 16px;

View File

@ -26,6 +26,7 @@ export interface MsSearchSelectProps {
valueKey?: string; // 选项的 value 字段名,默认为 value valueKey?: string; // 选项的 value 字段名,默认为 value
labelKey?: string; // 选项的 label 字段名,默认为 label labelKey?: string; // 选项的 label 字段名,默认为 label
options: SelectOptionData[]; options: SelectOptionData[];
objectValue?: boolean; // 是否使用选项对象作为 value
multiple?: boolean; // 是否多选 multiple?: boolean; // 是否多选
atLeastOne?: boolean; // 是否至少选择一个,多选模式下有效 atLeastOne?: boolean; // 是否至少选择一个,多选模式下有效
remoteFieldsMap?: RemoteFieldsMap; // 远程模式下的结果 key 映射,例如 { value: 'id' },表示远程请求时,会将返回结果的 id 赋值到 value 字段 remoteFieldsMap?: RemoteFieldsMap; // 远程模式下的结果 key 映射,例如 { value: 'id' },表示远程请求时,会将返回结果的 id 赋值到 value 字段
@ -217,7 +218,9 @@ export default defineComponent(
function handleSelectAllChange(val: boolean) { function handleSelectAllChange(val: boolean) {
isSelectAll.value = val; isSelectAll.value = val;
if (val) { if (val) {
innerValue.value = [...filterOptions.value]; innerValue.value = props.objectValue
? [...filterOptions.value]
: filterOptions.value.map((e) => e[props.valueKey || 'value']);
emit('update:modelValue', innerValue.value); emit('update:modelValue', innerValue.value);
} else { } else {
innerValue.value = []; innerValue.value = [];
@ -256,7 +259,7 @@ export default defineComponent(
<a-tooltip content={item.tooltipContent} mouse-enter-delay={500}> <a-tooltip content={item.tooltipContent} mouse-enter-delay={500}>
<a-option <a-option
key={item[props.valueKey || 'value']} key={item[props.valueKey || 'value']}
value={item} value={props.objectValue ? item : item[props.valueKey || 'value']}
tag-props={ tag-props={
props.multiple && props.atLeastOne props.multiple && props.atLeastOne
? { closable: Array.isArray(innerValue.value) && innerValue.value.length > 1 } ? { closable: Array.isArray(innerValue.value) && innerValue.value.length > 1 }
@ -375,7 +378,7 @@ export default defineComponent(
class="one-line-text" class="one-line-text"
style={singleTagMaxWidth.value > 0 ? { maxWidth: `${singleTagMaxWidth.value}px` } : {}} style={singleTagMaxWidth.value > 0 ? { maxWidth: `${singleTagMaxWidth.value}px` } : {}}
> >
{data.label} {slots.label ? slots.label(data) : data.label}
</div> </div>
</a-tooltip> </a-tooltip>
), ),
@ -411,6 +414,7 @@ export default defineComponent(
'fallbackOption', 'fallbackOption',
'labelKey', 'labelKey',
'atLeastOne', 'atLeastOne',
'objectValue',
], ],
emits: ['update:modelValue', 'remoteSearch', 'popupVisibleChange', 'update:loading', 'remove'], emits: ['update:modelValue', 'remoteSearch', 'popupVisibleChange', 'update:loading', 'remove'],
} }

View File

@ -235,6 +235,7 @@
(e: 'dataIndexChange', value: string): void; (e: 'dataIndexChange', value: string): void;
(e: 'update:count', value: number): void; // FilterIcon (e: 'update:count', value: number): void; // FilterIcon
(e: 'update:rowCount', value: number): void; // MsBaseTable (e: 'update:rowCount', value: number): void; // MsBaseTable
(e: 'reset'): void;
}>(); }>();
const isMultipleSelect = (dataIndex: string) => { const isMultipleSelect = (dataIndex: string) => {
@ -316,6 +317,7 @@
const handleReset = () => { const handleReset = () => {
formRef.value?.resetFields(); formRef.value?.resetFields();
formModel.list = [getInitItem()]; formModel.list = [getInitItem()];
emit('reset');
}; };
/** /**
* @description 筛选 * @description 筛选

View File

@ -42,6 +42,7 @@
class="mt-[8px]" class="mt-[8px]"
@on-search="handleFilter" @on-search="handleFilter"
@data-index-change="dataIndexChange" @data-index-change="dataIndexChange"
@reset="emit('reset')"
/> />
</template> </template>
@ -69,6 +70,7 @@
(e: 'keywordSearch', value: string | undefined): void; // innerKeyword (e: 'keywordSearch', value: string | undefined): void; // innerKeyword
(e: 'advSearch', value: FilterResult): void; // (e: 'advSearch', value: FilterResult): void; //
(e: 'dataIndexChange', value: string): void; // (e: 'dataIndexChange', value: string): void; //
(e: 'reset'): void;
}>(); }>();
const { t } = useI18n(); const { t } = useI18n();
@ -82,7 +84,6 @@
}; };
const handleFilter = (filter: FilterResult) => { const handleFilter = (filter: FilterResult) => {
console.log('filter', filter);
emit('advSearch', filter); emit('advSearch', filter);
}; };

View File

@ -4,6 +4,9 @@
type="icon-icon_that_person" type="icon-icon_that_person"
:size="props.size" :size="props.size"
class="text-[var(--color-text-4)]" class="text-[var(--color-text-4)]"
:style="{
fontSize: `${props.size}px !important`,
}"
/> />
<a-avatar <a-avatar
v-else-if="props.avatar === 'word'" v-else-if="props.avatar === 'word'"

View File

@ -162,7 +162,7 @@
const handleConfirm = async () => { const handleConfirm = async () => {
await validateForm(); await validateForm();
if (props.isDelete) { if (props.isDelete) {
emits('confirm'); emits('confirm', undefined, handleCancel);
} else { } else {
emits('confirm', form.value, handleCancel); emits('confirm', form.value, handleCancel);
} }

View File

@ -85,6 +85,7 @@
defineProps<{ defineProps<{
raw?: string; raw?: string;
uploadImage?: (file: File) => Promise<any>; uploadImage?: (file: File) => Promise<any>;
maxHeight?: string;
}>(), }>(),
{ {
raw: '', raw: '',
@ -112,7 +113,6 @@
const uploadQueue: queueAsPromised<Task> = fastq.promise(asyncWorker, 1); const uploadQueue: queueAsPromised<Task> = fastq.promise(asyncWorker, 1);
const { currentLocale } = useLocale(); const { currentLocale } = useLocale();
const locale = computed(() => currentLocale.value as 'zh-CN' | 'en-US');
watch( watch(
() => props.raw, () => props.raw,
@ -359,20 +359,30 @@
onBeforeUnmount(() => { onBeforeUnmount(() => {
editor.value?.destroy(); editor.value?.destroy();
}); });
const contentStyles = computed(() => {
return {
maxHeight: props.maxHeight || '200px',
overflow: 'auto',
};
});
</script> </script>
<template> <template>
<div class="rich-wrapper flex w-full"> <div class="rich-wrapper flex w-full">
<AttachmentSelectorModal v-model:visible="attachmentSelectorModal" /> <AttachmentSelectorModal v-model:visible="attachmentSelectorModal" />
<RichTextEditor v-if="editor" :editor="editor" :locale="locale" /> <RichTextEditor v-if="editor" :editor="editor" :content-styles="contentStyles" :locale="currentLocale" />
</div> </div>
</template> </template>
<style scoped lang="less"> <style scoped lang="less">
.rich-wrapper { .rich-wrapper {
position: relative; @apply relative overflow-hidden;
border: 1px solid var(--color-text-n8); border: 1px solid var(--color-text-n8);
border-radius: var(--border-radius-small);
:deep(.halo-rich-text-editor .ProseMirror) { :deep(.halo-rich-text-editor .ProseMirror) {
padding: 16px 24px !important;
p:first-child { p:first-child {
margin-top: 0; margin-top: 0;
} }
@ -383,21 +393,22 @@
color: var(--color-text-3) !important; color: var(--color-text-3) !important;
} }
} }
// :deep(.editor-content) {
:deep(.editor-header + div > div) { .ms-scroll-bar();
&::-webkit-scrollbar { }
width: 6px !important; </style>
height: 4px !important;
<style lang="less">
.v-popper__popper {
.v-popper__inner {
.drop-shadow {
.ms-scroll-bar();
}
} }
&::-webkit-scrollbar-thumb { }
border-radius: 8px !important; .tippy-box {
background: var(--color-text-input-border) !important; .command-items {
} .ms-scroll-bar();
&::-webkit-scrollbar-thumb:hover {
background: #a1a7b0 !important;
}
&&::-webkit-scrollbar-track {
@apply bg-white !important;
} }
} }
</style> </style>

View File

@ -150,6 +150,9 @@
:deep(.arco-split-pane) { :deep(.arco-split-pane) {
@apply relative overflow-hidden; @apply relative overflow-hidden;
} }
// :deep(.arco-split-pane-second) {
// @apply z-10;
// }
.animating { .animating {
:deep(.arco-split-pane) { :deep(.arco-split-pane) {
@apply relative overflow-hidden; @apply relative overflow-hidden;
@ -207,12 +210,12 @@
.vertical-expand-line { .vertical-expand-line {
@apply relative z-10 flex items-center justify-center bg-transparent; @apply relative z-10 flex items-center justify-center bg-transparent;
&::before { &::before {
@apply absolute w-full; @apply absolute w-full bg-transparent;
margin-bottom: -4px; margin-bottom: -4px;
height: 4px; height: 4px;
box-shadow: 0 -1px 4px 0 rgb(31 35 41 / 10%), 0 -1px 4px 0 rgb(255 255 255), 0 -1px 4px 0 rgb(255 255 255), box-shadow: 0 -2px 2px 0 rgb(31 35 41 / 10%), 0 -4px 4px 0 rgb(255 255 255), 0 -4px 4px 0 rgb(255 255 255),
0 -1px 4px 0 rgb(255 255 255); 0 -4px 4px 0 rgb(255 255 255);
content: ''; content: '';
} }
// .expand-icon--vertical { // .expand-icon--vertical {

View File

@ -94,6 +94,8 @@ export default function useTableProps<T>(
const keyword = ref(''); const keyword = ref('');
// 高级筛选 // 高级筛选
const advanceFilter = reactive<FilterResult>({ accordBelow: 'AND', combine: {} }); const advanceFilter = reactive<FilterResult>({ accordBelow: 'AND', combine: {} });
// 表格请求参数集合
const tableQueryParams = ref<TableQueryParams>({});
// 是否分页 // 是否分页
if (propsRes.value.showPagination) { if (propsRes.value.showPagination) {
@ -193,7 +195,7 @@ export default function useTableProps<T>(
try { try {
if (loadListFunc) { if (loadListFunc) {
setLoading(true); setLoading(true);
const data = await loadListFunc({ tableQueryParams.value = {
current, current,
pageSize: currentPageSize, pageSize: currentPageSize,
sort: sortItem.value, sort: sortItem.value,
@ -202,7 +204,8 @@ export default function useTableProps<T>(
combine: advanceFilter.combine, combine: advanceFilter.combine,
searchMode: advanceFilter.accordBelow, searchMode: advanceFilter.accordBelow,
...loadListParams.value, ...loadListParams.value,
}); };
const data = await loadListFunc(tableQueryParams.value);
const tmpArr = data.list; const tmpArr = data.list;
propsRes.value.data = tmpArr.map((item: MsTableDataItem<T>) => { propsRes.value.data = tmpArr.map((item: MsTableDataItem<T>) => {
if (item.updateTime) { if (item.updateTime) {
@ -312,6 +315,11 @@ export default function useTableProps<T>(
} }
}; };
// 获取表格请求参数
const getTableQueryParams = () => {
return tableQueryParams.value;
};
// 事件触发组 // 事件触发组
const propsEvent = ref({ const propsEvent = ref({
// 排序触发 // 排序触发
@ -442,5 +450,6 @@ export default function useTableProps<T>(
resetPagination, resetPagination,
getSelectedCount, getSelectedCount,
resetSelector, resetSelector,
getTableQueryParams,
}; };
} }

View File

@ -222,7 +222,7 @@
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (props.isAllScreen) resizeObserver.value.disconnect(); if (props.isAllScreen) resizeObserver.value?.disconnect();
}); });
</script> </script>

View File

@ -12,7 +12,7 @@
<template v-if="showProjectSelect"> <template v-if="showProjectSelect">
<a-divider direction="vertical" class="ml-0" /> <a-divider direction="vertical" class="ml-0" />
<a-select <a-select
class="w-auto min-w-[150px] max-w-[200px] focus-within:!bg-[var(--color-text-n8)] hover:!bg-[var(--color-text-n8)]" class="w-[200px] focus-within:!bg-[var(--color-text-n8)] hover:!bg-[var(--color-text-n8)]"
:default-value="appStore.currentProjectId" :default-value="appStore.currentProjectId"
:bordered="false" :bordered="false"
allow-search allow-search

View File

@ -0,0 +1,81 @@
import { ReviewItem } from '@/models/caseManagement/caseReview';
// 评审结果
export const reviewResultMap = {
UN_REVIEWED: {
label: 'caseManagement.caseReview.unReview',
color: 'var(--color-text-input-border)',
icon: 'icon-icon_block_filled',
},
UNDER_REVIEWED: {
label: 'caseManagement.caseReview.reviewing',
color: 'rgb(var(--link-6))',
icon: 'icon-icon_testing',
},
PASS: {
label: 'caseManagement.caseReview.reviewPass',
color: 'rgb(var(--success-6))',
icon: 'icon-icon_succeed_filled',
},
UN_PASS: {
label: 'caseManagement.caseReview.fail',
color: 'rgb(var(--danger-6))',
icon: 'icon-icon_close_filled',
},
RE_REVIEWED: {
label: 'caseManagement.caseReview.reReview',
color: 'rgb(var(--warning-6))',
icon: 'icon-icon_resubmit_filled',
},
} as const;
// 评审状态
export const reviewStatusMap = {
PREPARED: {
label: 'caseManagement.caseReview.unStart',
color: 'var(--color-text-n8)',
class: '!text-[var(--color-text-1)]',
},
UNDERWAY: {
label: 'caseManagement.caseReview.going',
color: 'rgb(var(--link-2))',
class: '!text-[rgb(var(--link-6))]',
},
COMPLETED: {
label: 'caseManagement.caseReview.finished',
color: 'rgb(var(--success-2))',
class: '!text-[rgb(var(--success-6))]',
},
ARCHIVED: {
label: 'caseManagement.caseReview.archived',
color: 'var(--color-text-n8)',
class: '!text-[var(--color-text-4)]',
},
} as const;
// 评审详情
export const reviewDefaultDetail: ReviewItem = {
id: '',
num: 0,
moduleId: '',
projectId: '',
reviewPassRule: 'SINGLE',
name: '',
status: 'PREPARED',
caseCount: 0,
passCount: 0,
unPassCount: 0,
reviewedCount: 0,
underReviewedCount: 0,
pos: 5000,
startTime: 0,
endTime: 0,
passRate: 0,
tags: [],
description: '',
createTime: 0,
createUser: '',
updateTime: 0,
updateUser: '',
reviewers: [],
reReviewedCount: 0,
followFlag: false,
};

View File

@ -38,7 +38,7 @@ export default function useSelect(config: UseSelectOption) {
let tagCount = 0; let tagCount = 0;
const values = Object.values(config.selectVal.value); const values = Object.values(config.selectVal.value);
for (let i = 0; i < values.length; i++) { for (let i = 0; i < values.length; i++) {
const tagWidth = values[i][config.labelKey || 'label'].length * 12; // 计算每个标签渲染出来的宽度文字大小在12px时宽度也是 12px const tagWidth = (values[i][config.labelKey || 'label']?.length || 0) * 12; // 计算每个标签渲染出来的宽度文字大小在12px时宽度也是 12px
if (lastWidth > tagWidth + 36) { if (lastWidth > tagWidth + 36) {
tagCount += 1; tagCount += 1;
lastWidth -= tagWidth + 36; // 36px是标签的边距、边框等宽度 lastWidth -= tagWidth + 36; // 36px是标签的边距、边框等宽度

View File

@ -74,6 +74,7 @@ export default {
'common.expandAll': 'Expand all', 'common.expandAll': 'Expand all',
'common.copy': 'Copy', 'common.copy': 'Copy',
'common.fork': 'Fork', 'common.fork': 'Fork',
'common.forked': 'Forked',
'common.more': 'More', 'common.more': 'More',
'common.recycle': 'Recycle Bin', 'common.recycle': 'Recycle Bin',
'common.new': 'New', 'common.new': 'New',
@ -87,4 +88,6 @@ export default {
'common.json': 'Object', 'common.json': 'Object',
'common.integer': 'Integer', 'common.integer': 'Integer',
'common.file': 'File', 'common.file': 'File',
'common.desc': 'Description',
'common.root': 'Default Module',
}; };

View File

@ -60,7 +60,7 @@ async function changeLocale(locale: LocaleType) {
export default function useLocale() { export default function useLocale() {
const { locale } = i18n.global; const { locale } = i18n.global;
const currentLocale = ref(locale); const currentLocale = ref(locale as LocaleType);
return { return {
currentLocale, currentLocale,

View File

@ -72,10 +72,11 @@ export default {
'common.quickAddMember': '快速添加成员', 'common.quickAddMember': '快速添加成员',
'common.export': '导出', 'common.export': '导出',
'common.import': '导入', 'common.import': '导入',
'common.collapseAll': '展开全部', 'common.collapseAll': '收起全部',
'common.expandAll': '收起全部', 'common.expandAll': '展开全部',
'common.copy': '复制', 'common.copy': '复制',
'common.fork': '关注', 'common.fork': '关注',
'common.forked': '已关注',
'common.more': '更多', 'common.more': '更多',
'common.recycle': '回收站', 'common.recycle': '回收站',
'common.new': '新增', 'common.new': '新增',
@ -89,4 +90,6 @@ export default {
'common.json': '对象', 'common.json': '对象',
'common.integer': '整数', 'common.integer': '整数',
'common.file': '文件', 'common.file': '文件',
'common.desc': '描述',
'common.root': '默认模块',
}; };

View File

@ -22,6 +22,17 @@ export interface ReviewModuleItem {
} }
// 评审类型 // 评审类型
export type ReviewPassRule = 'SINGLE' | 'MULTIPLE'; export type ReviewPassRule = 'SINGLE' | 'MULTIPLE';
// 用例评审关联用例入参
export interface BaseAssociateCaseRequest {
excludeIds: string[];
selectIds: string[];
selectAll: boolean;
condition: Record<string, any>;
moduleIds: string[];
versionId: string;
refId: string;
projectId: string;
}
// 评审 // 评审
export interface Review { export interface Review {
projectId: string; projectId: string;
@ -33,18 +44,22 @@ export interface Review {
tags: string[]; tags: string[];
description: string; description: string;
reviewers: string[]; // 评审人员 reviewers: string[]; // 评审人员
caseIds: string[]; // 关联用例 baseAssociateCaseRequest: BaseAssociateCaseRequest; // 关联用例
}
// 复制评审入参
export interface CopyReviewParams extends Omit<Review, 'baseAssociateCaseRequest'> {
copyId: string;
} }
// 更新评审入参 // 更新评审入参
export interface UpdateReviewParams extends Omit<Review, 'caseIds'> { export interface UpdateReviewParams extends Omit<Review, 'baseAssociateCaseRequest'> {
id: string; id: string;
} }
// 关联用例入参 // 关联用例入参
export interface AssociateReviewCaseParams { export interface AssociateReviewCaseParams {
reviewId: string; reviewId: string;
projectId: string; projectId: string;
caseIds: string[];
reviewers: string[]; reviewers: string[];
baseAssociateCaseRequest: BaseAssociateCaseRequest;
} }
// 关注/取消关注评审入参 // 关注/取消关注评审入参
export interface FollowReviewParams { export interface FollowReviewParams {
@ -66,12 +81,57 @@ export interface SortReviewParams {
moveMode: ReviewMoveMode; moveMode: ReviewMoveMode;
moveId: string; // 被移动的评审id moveId: string; // 被移动的评审id
} }
// 文件列表查询参数 // 评审状态, PREPARED: 待开始, UNDERWAY: 进行中, COMPLETED: 已完成, ARCHIVED: 已归档
export type ReviewStatus = 'PREPARED' | 'UNDERWAY' | 'COMPLETED' | 'ARCHIVED';
// 评审结果UN_REVIEWED未评审UNDER_REVIEWED评审中PASS通过UN_PASS未通过RE_REVIEWED重新提审
export type ReviewResult = 'UN_REVIEWED' | 'UNDER_REVIEWED' | 'PASS' | 'UN_PASS' | 'RE_REVIEWED';
// 评审列表查询参数
export interface ReviewListQueryParams extends TableQueryParams { export interface ReviewListQueryParams extends TableQueryParams {
moduleIds: string[]; moduleIds: string[];
projectId: string; projectId: string;
createByMe?: string;
reviewByMe?: string;
}
// 评审详情-用例列表查询参数
export interface ReviewDetailCaseListQueryParams extends TableQueryParams {
viewFlag: boolean; // 是否只看我的
reviewId: string;
}
// 评审详情-用例拖拽排序入参
export interface SortReviewCaseParams {
projectId: string;
targetId: string; // 目标用例id
moveMode: ReviewMoveMode;
moveId: string; // 被移动的用例id
reviewId: string; // 所属评审id
}
// 评审详情-批量评审用例
export interface BatchReviewCaseParams extends BatchApiParams {
reviewId: string; // 评审id
userId: string; // 用户id, 用来判断是否只看我的
reviewPassRule: ReviewPassRule; // 评审规则
status: ReviewResult; // 评审结果
content: string; // 评论内容
notifier: string; // 评论@的人的Id, 多个以';'隔开
}
// 评审详情-批量修改评审人
export interface BatchChangeReviewerParams extends BatchApiParams {
reviewId: string; // 评审id
userId: string; // 用户id, 用来判断是否只看我的
reviewerId: string[]; // 评审人员id
append: boolean; // 是否追加
}
// 评审详情-批量取消关联用例
export interface BatchCancelReviewCaseParams extends BatchApiParams {
reviewId: string; // 评审id
userId: string; // 用户id, 用来判断是否只看我的
}
export interface ReviewDetailReviewersItem {
avatar: string;
reviewId: string;
userId: string;
userName: string;
} }
export type ReviewStatus = 'PREPARED' | 'UNDERWAY' | 'COMPLETED' | 'ARCHIVED'; // 评审状态, PREPARED: 待开始, UNDERWAY: 进行中, COMPLETED: 已完成, ARCHIVED: 已归档
// 评审列表项 // 评审列表项
export interface ReviewItem { export interface ReviewItem {
id: string; id: string;
@ -86,13 +146,13 @@ export interface ReviewItem {
endTime: number; endTime: number;
caseCount: number; caseCount: number;
passRate: number; passRate: number;
tags: string; tags: string[];
description: string; description: string;
createTime: number; createTime: number;
createUser: string; createUser: string;
updateTime: number; updateTime: number;
updateUser: string; updateUser: string;
reviewers: string[]; reviewers: ReviewDetailReviewersItem[];
passCount: number; passCount: number;
unPassCount: number; unPassCount: number;
reReviewedCount: number; reReviewedCount: number;
@ -117,4 +177,44 @@ export interface ReviewUserItem {
createUser: string; createUser: string;
updateUser: string; updateUser: string;
deleted: boolean; deleted: boolean;
avatar: string;
}
// 评审详情-用例列表项
export interface ReviewCaseItem {
id: string;
name: string;
num: string;
caseId: string;
versionId: string;
versionName: string;
reviewers: string[];
reviewNames: string[];
status: string;
moduleId: string;
moduleName: string;
}
// 评审详情-提交评审入参
export interface CommitReviewResultParams {
projectId: string;
reviewId: string;
caseId: string;
reviewPassRule: ReviewPassRule;
status: ReviewResult;
content: string;
notifier: string;
}
// 评审详情-获取用例评审历史
export interface ReviewHistoryItem {
id: string;
reviewId: string;
caseId: string;
status: ReviewResult;
deleted: boolean; // 是否是取消关联或评审被删除的0-否1-是
notifier: string;
createUser: string;
createTime: number;
content: string;
userLogo: string;
userName: string;
contentText: string;
} }

View File

@ -1,6 +1,8 @@
import { TableQueryParams } from '@/models/common'; import { TableQueryParams } from '@/models/common';
import { StatusType } from '@/enums/caseEnum'; import { StatusType } from '@/enums/caseEnum';
import { ReviewResult } from './caseReview';
export interface ModulesTreeType { export interface ModulesTreeType {
id: string; id: string;
name: string; name: string;
@ -148,7 +150,7 @@ export interface BatchMoveOrCopyType {
excludeIds: string[] | undefined; excludeIds: string[] | undefined;
condition: Record<string, any>; condition: Record<string, any>;
} }
export type CaseEditType = 'STEP' | 'TEXT';
// 创建或者更新 // 创建或者更新
export interface CreateOrUpdateCase { export interface CreateOrUpdateCase {
id?: string; id?: string;
@ -156,7 +158,7 @@ export interface CreateOrUpdateCase {
templateId: string; templateId: string;
name: string; name: string;
prerequisite: string; // prerequisite prerequisite: string; // prerequisite
caseEditType: string; // 编辑模式:步骤模式/文本模式 caseEditType: CaseEditType; // 编辑模式:步骤模式/文本模式
steps: string; steps: string;
textDescription: string; textDescription: string;
expectedResult: string; // 预期结果 expectedResult: string; // 预期结果
@ -191,8 +193,8 @@ export interface DetailCase {
projectId: string; projectId: string;
templateId?: string; templateId?: string;
name: string; name: string;
reviewStatus?: string; reviewStatus: ReviewResult;
tags: any; tags: string[];
caseEditType: string; caseEditType: string;
versionId?: string; versionId?: string;
publicCase: boolean; publicCase: boolean;
@ -206,6 +208,7 @@ export interface DetailCase {
customFields: CustomAttributes[]; customFields: CustomAttributes[];
attachments?: AttachFileInfo[]; attachments?: AttachFileInfo[];
followFlag?: boolean; followFlag?: boolean;
functionalPriority: string;
[key: string]: any; [key: string]: any;
} }

View File

@ -88,6 +88,7 @@ const CaseManagement: AppRouteRecordRaw = {
isTopMenu: true, isTopMenu: true,
}, },
}, },
// 创建评审
{ {
path: 'caseManagementReviewCreate', path: 'caseManagementReviewCreate',
name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW_CREATE, name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW_CREATE,
@ -109,6 +110,7 @@ const CaseManagement: AppRouteRecordRaw = {
], ],
}, },
}, },
// 评审详情
{ {
path: 'caseManagementReviewDetail', path: 'caseManagementReviewDetail',
name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW_DETAIL, name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW_DETAIL,
@ -128,6 +130,7 @@ const CaseManagement: AppRouteRecordRaw = {
], ],
}, },
}, },
// 评审详情-用例详情
{ {
path: 'caseManagementReviewDetailCaseDetail', path: 'caseManagementReviewDetailCaseDetail',
name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW_DETAIL_CASE_DETAIL, name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW_DETAIL_CASE_DETAIL,

View File

@ -31,7 +31,7 @@ export interface AppRouteRecordRaw {
component: Component | string; component: Component | string;
children?: AppRouteRecordRaw[]; children?: AppRouteRecordRaw[];
alias?: string | string[]; alias?: string | string[];
props?: Record<string, any>; props?: Record<string, any> | boolean;
beforeEnter?: NavigationGuard | NavigationGuard[]; beforeEnter?: NavigationGuard | NavigationGuard[];
fullPath?: string; fullPath?: string;
} }

View File

@ -2,7 +2,7 @@
<a-popover position="tl" :disabled="!props.desc || props.desc.trim() === ''" class="ms-params-input-popover"> <a-popover position="tl" :disabled="!props.desc || props.desc.trim() === ''" class="ms-params-input-popover">
<template #content> <template #content>
<div class="param-popover-title"> <div class="param-popover-title">
{{ t('ms.apiTestDebug.desc') }} {{ t('apiTestDebug.desc') }}
</div> </div>
<div class="param-popover-value"> <div class="param-popover-value">
{{ props.desc }} {{ props.desc }}

View File

@ -2,15 +2,15 @@
<MsBaseTable v-bind="propsRes" id="headerTable" :hoverable="false" v-on="propsEvent"> <MsBaseTable v-bind="propsRes" id="headerTable" :hoverable="false" v-on="propsEvent">
<template #encodeTitle> <template #encodeTitle>
<div class="flex items-center text-[var(--color-text-3)]"> <div class="flex items-center text-[var(--color-text-3)]">
{{ t('ms.apiTestDebug.encode') }} {{ t('apiTestDebug.encode') }}
<a-tooltip> <a-tooltip>
<icon-question-circle <icon-question-circle
class="ml-[4px] text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]" class="ml-[4px] text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]"
size="16" size="16"
/> />
<template #content> <template #content>
<div>{{ t('ms.apiTestDebug.encodeTip1') }}</div> <div>{{ t('apiTestDebug.encodeTip1') }}</div>
<div>{{ t('ms.apiTestDebug.encodeTip2') }}</div> <div>{{ t('apiTestDebug.encodeTip2') }}</div>
</template> </template>
</a-tooltip> </a-tooltip>
</div> </div>
@ -19,7 +19,7 @@
<a-popover position="tl" :disabled="!record.name || record.name.trim() === ''" class="ms-params-input-popover"> <a-popover position="tl" :disabled="!record.name || record.name.trim() === ''" class="ms-params-input-popover">
<template #content> <template #content>
<div class="param-popover-title"> <div class="param-popover-title">
{{ t('ms.apiTestDebug.paramName') }} {{ t('apiTestDebug.paramName') }}
</div> </div>
<div class="param-popover-value"> <div class="param-popover-value">
{{ record.name }} {{ record.name }}
@ -27,14 +27,14 @@
</template> </template>
<a-input <a-input
v-model:model-value="record.name" v-model:model-value="record.name"
:placeholder="t('ms.apiTestDebug.paramNamePlaceholder')" :placeholder="t('apiTestDebug.paramNamePlaceholder')"
class="param-input" class="param-input"
@input="(val) => addTableLine(val)" @input="(val) => addTableLine(val)"
/> />
</a-popover> </a-popover>
</template> </template>
<template #type="{ record }"> <template #type="{ record }">
<a-tooltip :content="t(record.required ? 'ms.apiTestDebug.paramRequired' : 'ms.apiTestDebug.paramNotRequired')"> <a-tooltip :content="t(record.required ? 'apiTestDebug.paramRequired' : 'apiTestDebug.paramNotRequired')">
<MsButton <MsButton
type="icon" type="icon"
:class="[ :class="[
@ -65,14 +65,14 @@
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<a-input-number <a-input-number
v-model:model-value="record.min" v-model:model-value="record.min"
:placeholder="t('ms.apiTestDebug.paramMin')" :placeholder="t('apiTestDebug.paramMin')"
class="param-input" class="param-input"
@input="(val) => addTableLine(val)" @input="(val) => addTableLine(val)"
></a-input-number> ></a-input-number>
<div class="mx-[4px]"></div> <div class="mx-[4px]"></div>
<a-input-number <a-input-number
v-model:model-value="record.max" v-model:model-value="record.max"
:placeholder="t('ms.apiTestDebug.paramMax')" :placeholder="t('apiTestDebug.paramMax')"
class="param-input" class="param-input"
@input="(val) => addTableLine(val)" @input="(val) => addTableLine(val)"
></a-input-number> ></a-input-number>
@ -127,7 +127,7 @@
<a-modal <a-modal
v-model:visible="showQuickInputParam" v-model:visible="showQuickInputParam"
:title="t('ms.paramsInput.value')" :title="t('ms.paramsInput.value')"
:ok-text="t('ms.apiTestDebug.apply')" :ok-text="t('apiTestDebug.apply')"
class="ms-modal-form" class="ms-modal-form"
body-class="!p-0" body-class="!p-0"
:width="680" :width="680"
@ -145,7 +145,7 @@
<template #title> <template #title>
<div class="flex justify-between"> <div class="flex justify-between">
<div class="text-[var(--color-text-1)]"> <div class="text-[var(--color-text-1)]">
{{ t('ms.apiTestDebug.quickInputParamsTip') }} {{ t('apiTestDebug.quickInputParamsTip') }}
</div> </div>
</div> </div>
</template> </template>
@ -153,7 +153,7 @@
</a-modal> </a-modal>
<a-modal <a-modal
v-model:visible="showQuickInputDesc" v-model:visible="showQuickInputDesc"
:title="t('ms.apiTestDebug.desc')" :title="t('apiTestDebug.desc')"
:ok-text="t('common.save')" :ok-text="t('common.save')"
:ok-button-props="{ disabled: !quickInputDescValue || quickInputDescValue.trim() === '' }" :ok-button-props="{ disabled: !quickInputDescValue || quickInputDescValue.trim() === '' }"
class="ms-modal-form" class="ms-modal-form"
@ -166,7 +166,7 @@
> >
<a-textarea <a-textarea
v-model:model-value="quickInputDescValue" v-model:model-value="quickInputDescValue"
:placeholder="t('ms.apiTestDebug.descPlaceholder')" :placeholder="t('apiTestDebug.descPlaceholder')"
:max-length="255" :max-length="255"
show-word-limit show-word-limit
></a-textarea> ></a-textarea>
@ -203,7 +203,7 @@
const props = defineProps<{ const props = defineProps<{
params: any[]; params: any[];
columns: MsTableColumn; columns: MsTableColumn;
format?: RequestBodyFormat; format?: RequestBodyFormat | 'query' | 'rest';
scroll?: { scroll?: {
x?: number | string; x?: number | string;
y?: number | string; y?: number | string;
@ -257,7 +257,11 @@
}, },
]; ];
const typeOptions = computed(() => { const typeOptions = computed(() => {
if (props.format === RequestBodyFormat.X_WWW_FORM_URLENCODED) { if (
props.format === RequestBodyFormat.X_WWW_FORM_URLENCODED ||
props.format === 'query' ||
props.format === 'rest'
) {
return allType.filter((e) => e.value !== 'file' && e.value !== 'json'); return allType.filter((e) => e.value !== 'file' && e.value !== 'json');
} }
return allType; return allType;

View File

@ -0,0 +1,75 @@
<template>
<div class="mb-[8px] font-medium">{{ t('apiTestDebug.auth') }}</div>
<div class="rounded-[var(--border-radius-small)] border border-[var(--color-text-n8)] p-[16px]">
<div class="mb-[8px]">{{ t('apiTestDebug.authType') }}</div>
<a-radio-group v-model:model-value="authForm.authType" class="mb-[16px]" @change="authTypeChange">
<a-radio value="none">No Auth</a-radio>
<a-radio value="basic">Basic Auth</a-radio>
<a-radio value="digest">Digest Auth</a-radio>
</a-radio-group>
<a-form v-if="authForm.authType !== 'none'" ref="authFormRef" :model="authForm" layout="vertical">
<a-form-item
:label="t('apiTestDebug.account')"
:rules="[{ required: true, message: t('apiTestDebug.accountRequired') }]"
>
<a-input
v-model:model-value="authForm.account"
:placeholder="t('apiTestDebug.commonPlaceholder')"
class="w-[450px]"
/>
</a-form-item>
<a-form-item
:label="t('apiTestDebug.password')"
:rules="[{ required: true, message: t('apiTestDebug.passwordRequired') }]"
>
<a-input-password
v-model:model-value="authForm.password"
autocomplete="new-password"
:placeholder="t('apiTestDebug.commonPlaceholder')"
class="w-[450px]"
/>
</a-form-item>
</a-form>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core';
import { FormInstance } from '@arco-design/web-vue';
import { useI18n } from '@/hooks/useI18n';
interface AuthForm {
authType: string;
account: string;
password: string;
}
const props = defineProps<{
params: AuthForm;
}>();
const emit = defineEmits<{
(e: 'update:params', val: AuthForm): void;
(e: 'change', val: AuthForm): void;
}>();
const { t } = useI18n();
const authForm = useVModel(props, 'params', emit);
const authFormRef = ref<FormInstance>();
watch(
() => authForm.value,
() => {
emit('change', authForm.value);
},
{ deep: true }
);
function authTypeChange(val: string | number | boolean) {
if (val === 'none') {
authForm.value.account = '';
authForm.value.password = '';
}
}
</script>
<style lang="less" scoped></style>

View File

@ -1,12 +1,12 @@
<template> <template>
<a-button type="outline" size="mini" @click="showBatchAddParamDrawer = true"> <a-button type="outline" size="mini" @click="showBatchAddParamDrawer = true">
{{ t('ms.apiTestDebug.batchAdd') }} {{ t('apiTestDebug.batchAdd') }}
</a-button> </a-button>
<MsDrawer <MsDrawer
v-model:visible="showBatchAddParamDrawer" v-model:visible="showBatchAddParamDrawer"
:title="t('common.batchAdd')" :title="t('common.batchAdd')"
:width="680" :width="680"
:ok-text="t('ms.apiTestDebug.apply')" :ok-text="t('apiTestDebug.apply')"
disabled-width-drag disabled-width-drag
@confirm="applyBatchParams" @confirm="applyBatchParams"
> >
@ -22,10 +22,10 @@
<template #title> <template #title>
<div class="flex flex-col"> <div class="flex flex-col">
<div class="text-[12px] leading-[16px] text-[var(--color-text-4)]"> <div class="text-[12px] leading-[16px] text-[var(--color-text-4)]">
{{ t('ms.apiTestDebug.batchAddParamsTip') }} {{ t('apiTestDebug.batchAddParamsTip') }}
</div> </div>
<div class="text-[12px] leading-[16px] text-[var(--color-text-4)]"> <div class="text-[12px] leading-[16px] text-[var(--color-text-4)]">
{{ t('ms.apiTestDebug.batchAddParamsTip2') }} {{ t('apiTestDebug.batchAddParamsTip2') }}
</div> </div>
</div> </div>
</template> </template>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="mb-[8px] flex items-center justify-between"> <div class="mb-[8px] flex items-center justify-between">
<div class="font-medium">{{ t('ms.apiTestDebug.body') }}</div> <div class="font-medium">{{ t('apiTestDebug.body') }}</div>
<div class="flex items-center gap-[16px]"> <div class="flex items-center gap-[16px]">
<batchAddKeyVal v-if="showParamTable" :params="currentTableParams" @apply="handleBatchParamApply" /> <batchAddKeyVal v-if="showParamTable" :params="currentTableParams" @apply="handleBatchParamApply" />
<a-radio-group v-model:model-value="format" type="button" size="small" @change="formatChange"> <a-radio-group v-model:model-value="format" type="button" size="small" @change="formatChange">
@ -12,7 +12,7 @@
v-if="format === RequestBodyFormat.NONE" v-if="format === RequestBodyFormat.NONE"
class="flex h-[100px] items-center justify-center rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] text-[var(--color-text-4)]" class="flex h-[100px] items-center justify-center rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] text-[var(--color-text-4)]"
> >
{{ t('ms.apiTestDebug.noneBody') }} {{ t('apiTestDebug.noneBody') }}
</div> </div>
<paramTable <paramTable
v-else-if="showParamTable" v-else-if="showParamTable"
@ -23,6 +23,30 @@
:height-used="heightUsed" :height-used="heightUsed"
@change="handleParamTableChange" @change="handleParamTableChange"
/> />
<div v-else-if="format === RequestBodyFormat.BINARY">
<div class="mb-[16px] flex justify-between gap-[8px] bg-[var(--color-text-n9)] p-[12px]">
<a-input
v-model:model-value="innerParams.binaryDesc"
:placeholder="t('common.desc')"
:max-length="255"
show-word-limit
/>
</div>
<div class="flex items-center">
<a-switch v-model:model-value="innerParams.binarySend" class="mr-[8px]" size="small"></a-switch>
<span>{{ t('apiTestDebug.sendAsMainText') }}</span>
<a-tooltip position="right">
<template #content>
<div>{{ t('apiTestDebug.sendAsMainTextTip1') }}</div>
<div>{{ t('apiTestDebug.sendAsMainTextTip2') }}</div>
</template>
<icon-question-circle
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</div>
</div>
<div v-else class="flex h-[calc(100%-100px)]"> <div v-else class="flex h-[calc(100%-100px)]">
<MsCodeEditor <MsCodeEditor
v-model:model-value="currentBodyCode" v-model:model-value="currentBodyCode"
@ -35,10 +59,10 @@
<template #title> <template #title>
<div class="flex flex-col"> <div class="flex flex-col">
<div class="text-[12px] leading-[16px] text-[var(--color-text-4)]"> <div class="text-[12px] leading-[16px] text-[var(--color-text-4)]">
{{ t('ms.apiTestDebug.batchAddParamsTip') }} {{ t('apiTestDebug.batchAddParamsTip') }}
</div> </div>
<div class="text-[12px] leading-[16px] text-[var(--color-text-4)]"> <div class="text-[12px] leading-[16px] text-[var(--color-text-4)]">
{{ t('ms.apiTestDebug.batchAddParamsTip2') }} {{ t('apiTestDebug.batchAddParamsTip2') }}
</div> </div>
</div> </div>
</template> </template>
@ -65,6 +89,8 @@
json: string; json: string;
xml: string; xml: string;
binary: string; binary: string;
binaryDesc: string;
binarySend: boolean;
raw: string; raw: string;
} }
const props = defineProps<{ const props = defineProps<{
@ -83,35 +109,35 @@
const columns: MsTableColumn = [ const columns: MsTableColumn = [
{ {
title: 'ms.apiTestDebug.paramName', title: 'apiTestDebug.paramName',
dataIndex: 'name', dataIndex: 'name',
slotName: 'name', slotName: 'name',
}, },
{ {
title: 'ms.apiTestDebug.paramType', title: 'apiTestDebug.paramType',
dataIndex: 'type', dataIndex: 'type',
slotName: 'type', slotName: 'type',
width: 120, width: 120,
}, },
{ {
title: 'ms.apiTestDebug.paramValue', title: 'apiTestDebug.paramValue',
dataIndex: 'value', dataIndex: 'value',
slotName: 'value', slotName: 'value',
width: 240, width: 240,
}, },
{ {
title: 'ms.apiTestDebug.paramLengthRange', title: 'apiTestDebug.paramLengthRange',
dataIndex: 'lengthRange', dataIndex: 'lengthRange',
slotName: 'lengthRange', slotName: 'lengthRange',
width: 200, width: 200,
}, },
{ {
title: 'ms.apiTestDebug.desc', title: 'apiTestDebug.desc',
dataIndex: 'desc', dataIndex: 'desc',
slotName: 'desc', slotName: 'desc',
}, },
{ {
title: 'ms.apiTestDebug.encode', title: 'apiTestDebug.encode',
dataIndex: 'encode', dataIndex: 'encode',
slotName: 'encode', slotName: 'encode',
titleSlotName: 'encodeTitle', titleSlotName: 'encodeTitle',
@ -150,8 +176,10 @@
const format = ref(RequestBodyFormat.NONE); const format = ref(RequestBodyFormat.NONE);
const showParamTable = computed(() => { const showParamTable = computed(() => {
// FORM_DATAX_WWW_FORM_URLENCODED
return [RequestBodyFormat.FORM_DATA, RequestBodyFormat.X_WWW_FORM_URLENCODED].includes(format.value); return [RequestBodyFormat.FORM_DATA, RequestBodyFormat.X_WWW_FORM_URLENCODED].includes(format.value);
}); });
//
const currentTableParams = computed({ const currentTableParams = computed({
get() { get() {
if (format.value === RequestBodyFormat.FORM_DATA) { if (format.value === RequestBodyFormat.FORM_DATA) {
@ -167,6 +195,7 @@
} }
}, },
}); });
//
const currentBodyCode = computed({ const currentBodyCode = computed({
get() { get() {
if (format.value === RequestBodyFormat.JSON) { if (format.value === RequestBodyFormat.JSON) {
@ -187,6 +216,7 @@
} }
}, },
}); });
//
const currentCodeLanguage = computed(() => { const currentCodeLanguage = computed(() => {
if (format.value === RequestBodyFormat.JSON) { if (format.value === RequestBodyFormat.JSON) {
return 'json'; return 'json';

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="mb-[8px] flex items-center justify-between"> <div class="mb-[8px] flex items-center justify-between">
<div class="font-medium">{{ t('ms.apiTestDebug.header') }}</div> <div class="font-medium">{{ t('apiTestDebug.header') }}</div>
<batchAddKeyVal :params="innerParams" @apply="handleBatchParamApply" /> <batchAddKeyVal :params="innerParams" @apply="handleBatchParamApply" />
</div> </div>
<paramTable <paramTable
@ -37,17 +37,17 @@
const columns: MsTableColumn = [ const columns: MsTableColumn = [
{ {
title: 'ms.apiTestDebug.paramName', title: 'apiTestDebug.paramName',
dataIndex: 'name', dataIndex: 'name',
slotName: 'name', slotName: 'name',
}, },
{ {
title: 'ms.apiTestDebug.paramValue', title: 'apiTestDebug.paramValue',
dataIndex: 'value', dataIndex: 'value',
slotName: 'value', slotName: 'value',
}, },
{ {
title: 'ms.apiTestDebug.desc', title: 'apiTestDebug.desc',
dataIndex: 'desc', dataIndex: 'desc',
slotName: 'desc', slotName: 'desc',
}, },

View File

@ -35,25 +35,25 @@
</a-select> </a-select>
<a-input <a-input
v-model:model-value="debugUrl" v-model:model-value="debugUrl"
:placeholder="t('ms.apiTestDebug.urlPlaceholder')" :placeholder="t('apiTestDebug.urlPlaceholder')"
@change="handleActiveDebugChange" @change="handleActiveDebugChange"
/> />
</a-input-group> </a-input-group>
</div> </div>
<div class="ml-[16px]"> <div class="ml-[16px]">
<a-dropdown-button class="exec-btn"> <a-dropdown-button class="exec-btn">
{{ t('ms.apiTestDebug.serverExec') }} {{ t('apiTestDebug.serverExec') }}
<template #icon> <template #icon>
<icon-down /> <icon-down />
</template> </template>
<template #content> <template #content>
<a-doption>{{ t('ms.apiTestDebug.localExec') }}</a-doption> <a-doption>{{ t('apiTestDebug.localExec') }}</a-doption>
</template> </template>
</a-dropdown-button> </a-dropdown-button>
<a-button type="secondary"> <a-button type="secondary">
<div class="flex items-center"> <div class="flex items-center">
{{ t('common.save') }} {{ t('common.save') }}
<div class="text-[var(--color-text-4)]">(<icon-command size="14" /> + S)</div> <div class="text-[var(--color-text-4)]">(<icon-command size="14" />+S)</div>
</div> </div>
</a-button> </a-button>
</div> </div>
@ -88,6 +88,30 @@
:second-box-height="secondBoxHeight" :second-box-height="secondBoxHeight"
@change="handleActiveDebugChange" @change="handleActiveDebugChange"
/> />
<debugQuery
v-else-if="activeDebug.activeTab === RequestComposition.QUERY"
v-model:params="activeDebug.queryParams"
:layout="activeLayout"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<debugRest
v-else-if="activeDebug.activeTab === RequestComposition.REST"
v-model:params="activeDebug.restParams"
:layout="activeLayout"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<debugAuth
v-else-if="activeDebug.activeTab === RequestComposition.AUTH"
v-model:params="activeDebug.authParams"
@change="handleActiveDebugChange"
/>
<debugSetting
v-else-if="activeDebug.activeTab === RequestComposition.SETTING"
v-model:params="activeDebug.setting"
@change="handleActiveDebugChange"
/>
</div> </div>
</template> </template>
<template #second> <template #second>
@ -106,18 +130,19 @@
<icon-right :size="12" /> <icon-right :size="12" />
</MsButton> </MsButton>
</template> </template>
<div class="ml-[4px] mr-[24px] font-medium">{{ t('ms.apiTestDebug.responseContent') }}</div> <div class="ml-[4px] mr-[24px] font-medium">{{ t('apiTestDebug.responseContent') }}</div>
<a-radio-group <a-radio-group
v-model:model-value="activeLayout" v-model:model-value="activeLayout"
type="button" type="button"
size="small" size="small"
@change="handleActiveLayoutChange" @change="handleActiveLayoutChange"
> >
<a-radio value="vertical">{{ t('ms.apiTestDebug.vertical') }}</a-radio> <a-radio value="vertical">{{ t('apiTestDebug.vertical') }}</a-radio>
<a-radio value="horizontal">{{ t('ms.apiTestDebug.horizontal') }}</a-radio> <a-radio value="horizontal">{{ t('apiTestDebug.horizontal') }}</a-radio>
</a-radio-group> </a-radio-group>
</div> </div>
</div> </div>
<div class="p-[16px]"></div>
</template> </template>
</MsSplitBox> </MsSplitBox>
</div> </div>
@ -131,8 +156,12 @@
import { TabItem } from '@/components/pure/ms-editable-tab/types'; import { TabItem } from '@/components/pure/ms-editable-tab/types';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue'; import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import apiMethodName from '../../../components/apiMethodName.vue'; import apiMethodName from '../../../components/apiMethodName.vue';
import debugAuth from './auth.vue';
import debugBody, { BodyParams } from './body.vue'; import debugBody, { BodyParams } from './body.vue';
import debugHeader from './header.vue'; import debugHeader from './header.vue';
import debugQuery from './query.vue';
import debugRest from './rest.vue';
import debugSetting from './setting.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { registerCatchSaveShortcut, removeCatchSaveShortcut } from '@/utils/event'; import { registerCatchSaveShortcut, removeCatchSaveShortcut } from '@/utils/event';
@ -150,6 +179,8 @@
json: '', json: '',
xml: '', xml: '',
binary: '', binary: '',
binaryDesc: '',
binarySend: false,
raw: '', raw: '',
}; };
const debugTabs = ref<TabItem[]>([ const debugTabs = ref<TabItem[]>([
@ -157,12 +188,25 @@
id: initDefaultId, id: initDefaultId,
moduleProtocol: 'http', moduleProtocol: 'http',
activeTab: RequestComposition.HEADER, activeTab: RequestComposition.HEADER,
label: t('ms.apiTestDebug.newApi'), label: t('apiTestDebug.newApi'),
closable: true, closable: true,
method: RequestMethods.GET, method: RequestMethods.GET,
unSave: false, unSave: false,
headerParams: [], headerParams: [],
bodyParams: cloneDeep(defaultBodyParams), bodyParams: cloneDeep(defaultBodyParams),
queryParams: [],
restParams: [],
authParams: {
authType: 'none',
account: '',
password: '',
},
setting: {
connectTimeout: 60000,
responseTimeout: 60000,
certificateAlias: '',
redirect: 'follow',
},
}, },
]); ]);
const debugUrl = ref(''); const debugUrl = ref('');
@ -182,22 +226,35 @@
id, id,
moduleProtocol: 'http', moduleProtocol: 'http',
activeTab: RequestComposition.HEADER, activeTab: RequestComposition.HEADER,
label: t('ms.apiTestDebug.newApi'), label: t('apiTestDebug.newApi'),
closable: true, closable: true,
method: RequestMethods.GET, method: RequestMethods.GET,
unSave: false, unSave: false,
headerParams: [], headerParams: [],
bodyParams: cloneDeep(defaultBodyParams), bodyParams: cloneDeep(defaultBodyParams),
queryParams: [],
restParams: [],
authParams: {
authType: 'none',
account: '',
password: '',
},
setting: {
connectTimeout: 60000,
responseTimeout: 60000,
certificateAlias: '',
redirect: 'follow',
},
}); });
activeTab.value = id; activeTab.value = id;
} }
function closeDebugTab(tab: TabItem) { function closeDebugTab(tab: TabItem) {
const index = debugTabs.value.findIndex((item) => item.id === tab.id); const index = debugTabs.value.findIndex((item) => item.id === tab.id);
debugTabs.value.splice(index, 1);
if (activeTab.value === tab.id) { if (activeTab.value === tab.id) {
activeTab.value = debugTabs.value[0]?.id || ''; activeTab.value = debugTabs.value[0]?.id || '';
} }
debugTabs.value.splice(index, 1);
} }
const moreActionList = [ const moreActionList = [
@ -214,11 +271,11 @@
const contentTabList = [ const contentTabList = [
{ {
value: RequestComposition.HEADER, value: RequestComposition.HEADER,
label: t('ms.apiTestDebug.header'), label: t('apiTestDebug.header'),
}, },
{ {
value: RequestComposition.BODY, value: RequestComposition.BODY,
label: t('ms.apiTestDebug.body'), label: t('apiTestDebug.body'),
}, },
{ {
value: RequestComposition.QUERY, value: RequestComposition.QUERY,
@ -230,23 +287,23 @@
}, },
{ {
value: RequestComposition.PREFIX, value: RequestComposition.PREFIX,
label: t('ms.apiTestDebug.prefix'), label: t('apiTestDebug.prefix'),
}, },
{ {
value: RequestComposition.POST_CONDITION, value: RequestComposition.POST_CONDITION,
label: t('ms.apiTestDebug.postCondition'), label: t('apiTestDebug.postCondition'),
}, },
{ {
value: RequestComposition.ASSERTION, value: RequestComposition.ASSERTION,
label: t('ms.apiTestDebug.assertion'), label: t('apiTestDebug.assertion'),
}, },
{ {
value: RequestComposition.AUTH, value: RequestComposition.AUTH,
label: t('ms.apiTestDebug.auth'), label: t('apiTestDebug.auth'),
}, },
{ {
value: RequestComposition.SETTING, value: RequestComposition.SETTING,
label: t('ms.apiTestDebug.setting'), label: t('apiTestDebug.setting'),
}, },
]; ];
@ -292,6 +349,7 @@
function handleActiveLayoutChange() { function handleActiveLayoutChange() {
isExpanded.value = true; isExpanded.value = true;
splitBoxSize.value = 0.6; splitBoxSize.value = 0.6;
splitBoxRef.value?.expand(0.6);
} }
function saveDebug() { function saveDebug() {

View File

@ -0,0 +1,133 @@
<template>
<div class="mb-[8px] flex items-center justify-between">
<div class="flex items-center gap-[4px]">
<div class="font-medium">Query</div>
<a-tooltip :content="t('apiTestDebug.queryTip')" position="right">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</div>
<batchAddKeyVal :params="innerParams" @apply="handleBatchParamApply" />
</div>
<paramTable
v-model:params="innerParams"
:columns="columns"
:height-used="heightUsed"
:scroll="{ minWidth: 1160 }"
format="query"
@change="handleParamTableChange"
/>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import paramTable from '../../../components/paramTable.vue';
import batchAddKeyVal from './batchAddKeyVal.vue';
import { useI18n } from '@/hooks/useI18n';
const props = defineProps<{
params: any[];
layout: 'horizontal' | 'vertical';
secondBoxHeight: number;
}>();
const emit = defineEmits<{
(e: 'update:params', value: any[]): void;
(e: 'change'): void; //
}>();
const { t } = useI18n();
const innerParams = useVModel(props, 'params', emit);
const columns: MsTableColumn = [
{
title: 'apiTestDebug.paramName',
dataIndex: 'name',
slotName: 'name',
},
{
title: 'apiTestDebug.paramType',
dataIndex: 'type',
slotName: 'type',
width: 120,
},
{
title: 'apiTestDebug.paramValue',
dataIndex: 'value',
slotName: 'value',
},
{
title: 'apiTestDebug.paramLengthRange',
dataIndex: 'lengthRange',
slotName: 'lengthRange',
width: 200,
},
{
title: 'apiTestDebug.encode',
dataIndex: 'encode',
slotName: 'encode',
titleSlotName: 'encodeTitle',
},
{
title: 'apiTestDebug.desc',
dataIndex: 'desc',
slotName: 'desc',
},
{
title: '',
slotName: 'operation',
fixed: 'right',
width: 50,
},
];
const heightUsed = ref<number | undefined>(undefined);
watch(
() => props.layout,
(val) => {
heightUsed.value = val === 'horizontal' ? 422 : 422 + props.secondBoxHeight;
},
{
immediate: true,
}
);
watch(
() => props.secondBoxHeight,
(val) => {
if (props.layout === 'vertical') {
heightUsed.value = 422 + val;
}
},
{
immediate: true,
}
);
/**
* 批量参数代码转换为参数表格数据
*/
function handleBatchParamApply(resultArr: any[]) {
if (resultArr.length < innerParams.value.length) {
innerParams.value.splice(0, innerParams.value.length - 1, ...resultArr);
} else {
innerParams.value = [...resultArr, innerParams.value[innerParams.value.length - 1]];
}
emit('change');
}
function handleParamTableChange(resultArr: any[], isInit?: boolean) {
innerParams.value = [...resultArr];
if (!isInit) {
emit('change');
}
}
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,7 @@
<template>
<div> </div>
</template>
<script setup lang="ts"></script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,133 @@
<template>
<div class="mb-[8px] flex items-center justify-between">
<div class="flex items-center gap-[4px]">
<div class="font-medium">Rest</div>
<a-tooltip :content="t('apiTestDebug.restTip', { id: '{id}' })" position="right">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</div>
<batchAddKeyVal :params="innerParams" @apply="handleBatchParamApply" />
</div>
<paramTable
v-model:params="innerParams"
:columns="columns"
:height-used="heightUsed"
:scroll="{ minWidth: 1160 }"
format="query"
@change="handleParamTableChange"
/>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import paramTable from '../../../components/paramTable.vue';
import batchAddKeyVal from './batchAddKeyVal.vue';
import { useI18n } from '@/hooks/useI18n';
const props = defineProps<{
params: any[];
layout: 'horizontal' | 'vertical';
secondBoxHeight: number;
}>();
const emit = defineEmits<{
(e: 'update:params', value: any[]): void;
(e: 'change'): void; //
}>();
const { t } = useI18n();
const innerParams = useVModel(props, 'params', emit);
const columns: MsTableColumn = [
{
title: 'apiTestDebug.paramName',
dataIndex: 'name',
slotName: 'name',
},
{
title: 'apiTestDebug.paramType',
dataIndex: 'type',
slotName: 'type',
width: 120,
},
{
title: 'apiTestDebug.paramValue',
dataIndex: 'value',
slotName: 'value',
},
{
title: 'apiTestDebug.paramLengthRange',
dataIndex: 'lengthRange',
slotName: 'lengthRange',
width: 200,
},
{
title: 'apiTestDebug.encode',
dataIndex: 'encode',
slotName: 'encode',
titleSlotName: 'encodeTitle',
},
{
title: 'apiTestDebug.desc',
dataIndex: 'desc',
slotName: 'desc',
},
{
title: '',
slotName: 'operation',
fixed: 'right',
width: 50,
},
];
const heightUsed = ref<number | undefined>(undefined);
watch(
() => props.layout,
(val) => {
heightUsed.value = val === 'horizontal' ? 422 : 422 + props.secondBoxHeight;
},
{
immediate: true,
}
);
watch(
() => props.secondBoxHeight,
(val) => {
if (props.layout === 'vertical') {
heightUsed.value = 422 + val;
}
},
{
immediate: true,
}
);
/**
* 批量参数代码转换为参数表格数据
*/
function handleBatchParamApply(resultArr: any[]) {
if (resultArr.length < innerParams.value.length) {
innerParams.value.splice(0, innerParams.value.length - 1, ...resultArr);
} else {
innerParams.value = [...resultArr, innerParams.value[innerParams.value.length - 1]];
}
emit('change');
}
function handleParamTableChange(resultArr: any[], isInit?: boolean) {
innerParams.value = [...resultArr];
if (!isInit) {
emit('change');
}
}
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,74 @@
<template>
<div class="pb-[24px]">
<div class="mb-[8px] font-medium">{{ t('apiTestDebug.auth') }}</div>
<div class="rounded-[var(--border-radius-small)] border border-[var(--color-text-n8)] p-[16px]">
<div class="mb-[8px]">{{ t('apiTestDebug.setting') }}</div>
<a-form :model="settingForm" layout="vertical">
<a-form-item>
<template #label>
<div class="flex items-center">
{{ t('apiTestDebug.connectTimeout') }}
<div class="text-[var(--color-text-brand)]">(ms)</div>
</div>
</template>
<a-input-number v-model:model-value="settingForm.connectTimeout" mode="button" class="w-[160px]" />
</a-form-item>
<a-form-item>
<template #label>
<div class="flex items-center">
{{ t('apiTestDebug.responseTimeout') }}
<div class="text-[var(--color-text-brand)]">(ms)</div>
</div>
</template>
<a-input-number v-model:model-value="settingForm.responseTimeout" mode="button" class="w-[160px]" />
</a-form-item>
<a-form-item :label="t('apiTestDebug.certificateAlias')">
<a-input
v-model:model-value="settingForm.certificateAlias"
:placeholder="t('apiTestDebug.commonPlaceholder')"
class="w-[450px]"
/>
</a-form-item>
<a-form-item :label="t('apiTestDebug.redirect')">
<a-radio-group v-model:model-value="settingForm.redirect">
<a-radio value="follow">{{ t('apiTestDebug.follow') }}</a-radio>
<a-radio value="auto">{{ t('apiTestDebug.auto') }}</a-radio>
</a-radio-group>
</a-form-item>
</a-form>
</div>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core';
import { useI18n } from '@/hooks/useI18n';
interface SettingForm {
connectTimeout: number;
responseTimeout: number;
certificateAlias: string;
redirect: 'follow' | 'auto';
}
const props = defineProps<{
params: SettingForm;
}>();
const emit = defineEmits<{
(e: 'update:params', val: SettingForm): void;
(e: 'change', val: SettingForm): void;
}>();
const { t } = useI18n();
const settingForm = useVModel(props, 'params', emit);
watch(
() => settingForm.value,
() => {
emit('change', settingForm.value);
},
{ deep: true }
);
</script>
<style lang="less" scoped></style>

View File

@ -7,10 +7,10 @@
allow-clear allow-clear
/> />
<a-dropdown @select="handleSelect"> <a-dropdown @select="handleSelect">
<a-button type="primary">{{ t('ms.apiTestDebug.newApi') }}</a-button> <a-button type="primary">{{ t('apiTestDebug.newApi') }}</a-button>
<template #content> <template #content>
<a-doption value="newApi">{{ t('ms.apiTestDebug.newApi') }}</a-doption> <a-doption value="newApi">{{ t('apiTestDebug.newApi') }}</a-doption>
<a-doption value="import">{{ t('ms.apiTestDebug.importApi') }}</a-doption> <a-doption value="import">{{ t('apiTestDebug.importApi') }}</a-doption>
</template> </template>
</a-dropdown> </a-dropdown>
</div> </div>
@ -46,7 +46,7 @@
:node-more-actions="folderMoreActions" :node-more-actions="folderMoreActions"
:default-expand-all="isExpandAll" :default-expand-all="isExpandAll"
:expand-all="isExpandAll" :expand-all="isExpandAll"
:empty-text="t('ms.apiTestDebug.noMatchModule')" :empty-text="t('apiTestDebug.noMatchModule')"
:draggable="!props.isModal" :draggable="!props.isModal"
:virtual-list-props="virtualListProps" :virtual-list-props="virtualListProps"
:field-names="{ :field-names="{

View File

@ -1,31 +1,31 @@
export default { export default {
'ms.apiTestDebug.newApi': 'New request', 'apiTestDebug.newApi': 'New request',
'ms.apiTestDebug.importApi': 'Import request', 'apiTestDebug.importApi': 'Import request',
'ms.apiTestDebug.urlPlaceholder': 'Please enter the full URL including http or https', 'apiTestDebug.urlPlaceholder': 'Please enter the full URL including http or https',
'ms.apiTestDebug.serverExec': 'Server execution', 'apiTestDebug.serverExec': 'Server execution',
'ms.apiTestDebug.localExec': 'Local execution', 'apiTestDebug.localExec': 'Local execution',
'ms.apiTestDebug.noMatchModule': 'No matching module data yet', 'apiTestDebug.noMatchModule': 'No matching module data yet',
'ms.apiTestDebug.header': 'Header', 'apiTestDebug.header': 'Header',
'ms.apiTestDebug.body': 'Body', 'apiTestDebug.body': 'Body',
'ms.apiTestDebug.prefix': 'Prefix', 'apiTestDebug.prefix': 'Prefix',
'ms.apiTestDebug.postCondition': 'Post condition', 'apiTestDebug.postCondition': 'Post condition',
'ms.apiTestDebug.assertion': 'Assertion', 'apiTestDebug.assertion': 'Assertion',
'ms.apiTestDebug.auth': 'Auth', 'apiTestDebug.auth': 'Auth',
'ms.apiTestDebug.setting': 'Setting', 'apiTestDebug.setting': 'Setting',
'ms.apiTestDebug.batchAdd': 'Batch add', 'apiTestDebug.batchAdd': 'Batch add',
'ms.apiTestDebug.responseContent': 'Response content', 'apiTestDebug.responseContent': 'Response content',
'ms.apiTestDebug.vertical': 'Vertical layout', 'apiTestDebug.vertical': 'Vertical layout',
'ms.apiTestDebug.horizontal': 'Horizontal layout', 'apiTestDebug.horizontal': 'Horizontal layout',
'ms.apiTestDebug.paramName': 'Parameter name', 'apiTestDebug.paramName': 'Parameter name',
'ms.apiTestDebug.paramNamePlaceholder': 'Please enter parameter name', 'apiTestDebug.paramNamePlaceholder': 'Please enter parameter name',
'ms.apiTestDebug.paramValue': 'Parameter value', 'apiTestDebug.paramValue': 'Parameter value',
'ms.apiTestDebug.paramValuePlaceholder': 'Starting with {at}, double-click to quickly enter', 'apiTestDebug.paramValuePlaceholder': 'Starting with {at}, double-click to quickly enter',
'ms.apiTestDebug.paramValuePreview': 'Parameter preview', 'apiTestDebug.paramValuePreview': 'Parameter preview',
'ms.apiTestDebug.desc': 'Description', 'apiTestDebug.desc': 'Description',
'ms.apiTestDebug.apply': 'Apply', 'apiTestDebug.apply': 'Apply',
'ms.apiTestDebug.batchAddParamsTip': 'Writing format: parameter name: parameter value; such as nama: natural', 'apiTestDebug.batchAddParamsTip': 'Writing format: parameter name: parameter value; such as nama: natural',
'ms.apiTestDebug.batchAddParamsTip2': 'apiTestDebug.batchAddParamsTip2':
'Note: Multiple records are separated by newlines. Parameter names in batch addition are repeated. By default, the last data is the latest data.', 'Note: Multiple records are separated by newlines. Parameter names in batch addition are repeated. By default, the last data is the latest data.',
'ms.apiTestDebug.quickInputParamsTip': 'Support Mock/JMeter/Json/Text/String, etc.', 'apiTestDebug.quickInputParamsTip': 'Support Mock/JMeter/Json/Text/String, etc.',
'ms.apiTestDebug.descPlaceholder': 'Please enter content', 'apiTestDebug.descPlaceholder': 'Please enter content',
}; };

View File

@ -1,40 +1,57 @@
export default { export default {
'ms.apiTestDebug.newApi': '新建请求', 'apiTestDebug.newApi': '新建请求',
'ms.apiTestDebug.importApi': '导入请求', 'apiTestDebug.importApi': '导入请求',
'ms.apiTestDebug.urlPlaceholder': '请输入包含 http 或 https 的完整URL', 'apiTestDebug.urlPlaceholder': '请输入包含 http 或 https 的完整URL',
'ms.apiTestDebug.serverExec': '服务端执行', 'apiTestDebug.serverExec': '服务端执行',
'ms.apiTestDebug.localExec': '本地执行', 'apiTestDebug.localExec': '本地执行',
'ms.apiTestDebug.noMatchModule': '暂无匹配的模块数据', 'apiTestDebug.noMatchModule': '暂无匹配的模块数据',
'ms.apiTestDebug.header': '请求头', 'apiTestDebug.header': '请求头',
'ms.apiTestDebug.body': '请求体', 'apiTestDebug.body': '请求体',
'ms.apiTestDebug.prefix': '前置', 'apiTestDebug.prefix': '前置',
'ms.apiTestDebug.postCondition': '后置', 'apiTestDebug.postCondition': '后置',
'ms.apiTestDebug.assertion': '断言', 'apiTestDebug.assertion': '断言',
'ms.apiTestDebug.auth': '认证', 'apiTestDebug.auth': '认证',
'ms.apiTestDebug.setting': '设置', 'apiTestDebug.setting': '设置',
'ms.apiTestDebug.batchAdd': '批量添加', 'apiTestDebug.batchAdd': '批量添加',
'ms.apiTestDebug.responseContent': '响应内容', 'apiTestDebug.responseContent': '响应内容',
'ms.apiTestDebug.vertical': '上下布局', 'apiTestDebug.vertical': '上下布局',
'ms.apiTestDebug.horizontal': '左右布局', 'apiTestDebug.horizontal': '左右布局',
'ms.apiTestDebug.paramName': '参数名称', 'apiTestDebug.paramName': '参数名称',
'ms.apiTestDebug.paramNamePlaceholder': '请输入参数名称', 'apiTestDebug.paramNamePlaceholder': '请输入参数名称',
'ms.apiTestDebug.paramRequired': '必填', 'apiTestDebug.paramRequired': '必填',
'ms.apiTestDebug.paramNotRequired': '非必填', 'apiTestDebug.paramNotRequired': '非必填',
'ms.apiTestDebug.paramType': '类型', 'apiTestDebug.paramType': '类型',
'ms.apiTestDebug.paramValue': '参数值', 'apiTestDebug.paramValue': '参数值',
'ms.apiTestDebug.paramValuePlaceholder': '以{at}开始,双击可快速输入', 'apiTestDebug.paramValuePlaceholder': '以{at}开始,双击可快速输入',
'ms.apiTestDebug.paramLengthRange': '长度区间', 'apiTestDebug.paramLengthRange': '长度区间',
'ms.apiTestDebug.paramMin': '最小值', 'apiTestDebug.paramMin': '最小值',
'ms.apiTestDebug.paramMax': '最大值', 'apiTestDebug.paramMax': '最大值',
'ms.apiTestDebug.paramValuePreview': '参数预览', 'apiTestDebug.paramValuePreview': '参数预览',
'ms.apiTestDebug.desc': '描述', 'apiTestDebug.desc': '描述',
'ms.apiTestDebug.encode': '编码', 'apiTestDebug.encode': '编码',
'ms.apiTestDebug.encodeTip1': '开启:使用编码', 'apiTestDebug.encodeTip1': '开启:使用编码',
'ms.apiTestDebug.encodeTip2': '关闭:不使用编码', 'apiTestDebug.encodeTip2': '关闭:不使用编码',
'ms.apiTestDebug.apply': '应用', 'apiTestDebug.apply': '应用',
'ms.apiTestDebug.batchAddParamsTip': '书写格式:参数名:参数值;如 nama:natural', 'apiTestDebug.batchAddParamsTip': '书写格式:参数名:参数值;如 nama:natural',
'ms.apiTestDebug.batchAddParamsTip2': '注: 多条记录以换行分隔,批量添加里的参数名重复,默认以最后一条数据为最新数据', 'apiTestDebug.batchAddParamsTip2': '注: 多条记录以换行分隔,批量添加里的参数名重复,默认以最后一条数据为最新数据',
'ms.apiTestDebug.quickInputParamsTip': '支持Mock/JMeter/Json/Text/String等', 'apiTestDebug.quickInputParamsTip': '支持Mock/JMeter/Json/Text/String等',
'ms.apiTestDebug.descPlaceholder': '请输入内容', 'apiTestDebug.descPlaceholder': '请输入内容',
'ms.apiTestDebug.noneBody': '请求没有 Body', 'apiTestDebug.noneBody': '请求没有 Body',
'apiTestDebug.sendAsMainText': '作为正文发送',
'apiTestDebug.sendAsMainTextTip1': '开启:直接读取文件内容在响应体展示,如:图片格式的文件',
'apiTestDebug.sendAsMainTextTip2': '关闭:以下载文件的形式返回',
'apiTestDebug.queryTip': '地址栏中跟在 ?后面的参数,如 updateapi?id=112',
'apiTestDebug.restTip': '地址栏中被斜杠/分隔的参数如updateapi/{id}',
'apiTestDebug.authType': '认证方式',
'apiTestDebug.account': '账号',
'apiTestDebug.accountRequired': '账号不能为空',
'apiTestDebug.password': '密码',
'apiTestDebug.passwordRequired': '密码不能为空',
'apiTestDebug.commonPlaceholder': '请输入',
'apiTestDebug.connectTimeout': '连接超时',
'apiTestDebug.responseTimeout': '响应超时',
'apiTestDebug.certificateAlias': '证书别名',
'apiTestDebug.redirect': '重定向',
'apiTestDebug.follow': '跟随',
'apiTestDebug.auto': '自动',
}; };

View File

@ -281,10 +281,13 @@
() => props.stepList, () => props.stepList,
() => { () => {
stepData.value = props.stepList; stepData.value = props.stepList;
},
{
immediate: true,
} }
); );
onMounted(() => { onBeforeMount(() => {
setProps({ data: stepData.value }); setProps({ data: stepData.value });
}); });
</script> </script>

View File

@ -7,7 +7,11 @@
@save="saveHandler" @save="saveHandler"
@save-and-continue="saveHandler(true)" @save-and-continue="saveHandler(true)"
> >
<CaseTemplateDetail ref="caseModuleDetailRef" v-model:form-mode-value="caseDetailInfo" /> <CaseTemplateDetail
ref="caseModuleDetailRef"
v-model:form-mode-value="caseDetailInfo"
:case-id="(route.query.id as string)"
/>
<template #footerRight> <template #footerRight>
<div class="flex justify-end gap-[16px]"> <div class="flex justify-end gap-[16px]">
<a-button type="secondary" @click="cancelHandler">{{ t('mscard.defaultCancelText') }}</a-button> <a-button type="secondary" @click="cancelHandler">{{ t('mscard.defaultCancelText') }}</a-button>
@ -38,6 +42,7 @@
import useFeatureCaseStore from '@/store/modules/case/featureCase'; import useFeatureCaseStore from '@/store/modules/case/featureCase';
import { scrollIntoView } from '@/utils/dom'; import { scrollIntoView } from '@/utils/dom';
import { CreateOrUpdateCase } from '@/models/caseManagement/featureCase';
import { CaseManagementRouteEnum } from '@/enums/routeEnum'; import { CaseManagementRouteEnum } from '@/enums/routeEnum';
import Message from '@arco-design/web-vue/es/message'; import Message from '@arco-design/web-vue/es/message';
@ -51,8 +56,8 @@
const visitedKey = 'doNotNextTipCreateCase'; const visitedKey = 'doNotNextTipCreateCase';
const { getIsVisited } = useVisit(visitedKey); const { getIsVisited } = useVisit(visitedKey);
const caseDetailInfo = ref<Record<string, any>>({ const caseDetailInfo = ref({
request: {}, request: {} as CreateOrUpdateCase,
fileList: [], fileList: [],
}); });
@ -76,11 +81,10 @@
query: { organizationId: route.query.organizationId, projectId: route.query.projectId }, query: { organizationId: route.query.organizationId, projectId: route.query.projectId },
}); });
} else { } else {
const res = await createCaseRequest(caseDetailInfo.value);
if (isReview) { if (isReview) {
// TODO caseDetailInfo.value.request.reviewId = route.query.reviewId;
//
} }
const res = await createCaseRequest(caseDetailInfo.value);
createSuccessId.value = res.data.id; createSuccessId.value = res.data.id;
Message.success(route.params.mode === 'copy' ? t('ms.description.copySuccess') : t('common.addSuccess')); Message.success(route.params.mode === 'copy' ? t('ms.description.copySuccess') : t('common.addSuccess'));
featureCaseStore.setIsAlreadySuccess(true); featureCaseStore.setIsAlreadySuccess(true);
@ -96,16 +100,24 @@
} }
} }
if (isReview) { if (isReview) {
router.back();
return;
}
router.push({ name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE, query: { ...route.query } });
featureCaseStore.setIsAlreadySuccess(true);
isShowTip.value = !getIsVisited();
if (isShowTip.value && !route.query.id) {
router.push({ router.push({
name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW_DETAIL, name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE_CREATE_SUCCESS,
query: { query: {
id: route.query.reviewId, id: createSuccessId.value,
organizationId: route.query.organizationId, ...route.query,
projectId: route.query.projectId,
}, },
}); });
} }
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console
console.log(error); console.log(error);
} finally { } finally {
loading.value = false; loading.value = false;

View File

@ -295,6 +295,8 @@
tags: [], tags: [],
customFields: [], // customFields: [], //
relateFileMetaIds: [], // ID relateFileMetaIds: [], // ID
functionalPriority: '',
reviewStatus: 'UN_REVIEWED',
}; };
const detailInfo = ref<DetailCase>({ ...initDetail }); const detailInfo = ref<DetailCase>({ ...initDetail });

View File

@ -91,9 +91,9 @@
:request-fun="transferFileRequest" :request-fun="transferFileRequest"
:params="{ :params="{
projectId: currentProjectId, projectId: currentProjectId,
caseId:route.query.id as string, caseId: props.caseId,
fileId:item.uid, fileId: item.uid,
local:true local: true,
}" }"
@success="getCaseInfo()" @success="getCaseInfo()"
/> />
@ -119,7 +119,7 @@
{{ t('ms.upload.preview') }} {{ t('ms.upload.preview') }}
</MsButton> </MsButton>
<MsButton <MsButton
v-if="route.query.id" v-if="props.caseId"
type="button" type="button"
status="primary" status="primary"
class="!mr-[4px]" class="!mr-[4px]"
@ -128,7 +128,7 @@
{{ t('caseManagement.featureCase.download') }} {{ t('caseManagement.featureCase.download') }}
</MsButton> </MsButton>
<MsButton <MsButton
v-if="route.query.id && item.isUpdateFlag" v-if="props.caseId && item.isUpdateFlag"
type="button" type="button"
status="primary" status="primary"
@click="handleUpdateFile(item)" @click="handleUpdateFile(item)"
@ -278,6 +278,7 @@
const props = defineProps<{ const props = defineProps<{
formModeValue: Record<string, any>; // formModeValue: Record<string, any>; //
caseId: string; // id
}>(); }>();
const emit = defineEmits(['update:formModeValue', 'changeFile']); const emit = defineEmits(['update:formModeValue', 'changeFile']);
@ -327,6 +328,8 @@
tags: [], tags: [],
customFields: [], customFields: [],
relateFileMetaIds: [], relateFileMetaIds: [],
functionalPriority: '',
reviewStatus: 'UN_REVIEWED',
}; };
const form = ref<DetailCase | CreateOrUpdateCase>({ ...initForm }); const form = ref<DetailCase | CreateOrUpdateCase>({ ...initForm });
@ -411,7 +414,7 @@
fileList.value.push(...fileResultList); fileList.value.push(...fileResultList);
} }
const isEditOrCopy = computed(() => !!route.query.id); const isEditOrCopy = computed(() => !!props.caseId);
const attachmentsList = ref<AttachFileInfo[]>([]); const attachmentsList = ref<AttachFileInfo[]>([]);
// localitem // localitem
@ -473,14 +476,14 @@
if (item.status !== 'init') { if (item.status !== 'init') {
const res = await previewFile({ const res = await previewFile({
projectId: currentProjectId.value, projectId: currentProjectId.value,
caseId: route.query.id as string, caseId: props.caseId,
fileId: item.uid, fileId: item.uid,
local: item.local, local: item.local,
}); });
const blob = new Blob([res], { type: 'image/jpeg' }); const blob = new Blob([res], { type: 'image/jpeg' });
imageUrl.value = URL.createObjectURL(blob); imageUrl.value = URL.createObjectURL(blob);
} else { } else {
imageUrl.value = item.url as string; imageUrl.value = item.url || '';
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
@ -532,7 +535,7 @@
try { try {
isLoading.value = true; isLoading.value = true;
await getAllCaseFields(); await getAllCaseFields();
const detailResult: DetailCase = await getCaseDetail(route.query.id as string); const detailResult: DetailCase = await getCaseDetail(props.caseId);
const fileIds = (detailResult.attachments || []).map((item: any) => item.id); const fileIds = (detailResult.attachments || []).map((item: any) => item.id);
if (fileIds.length) { if (fileIds.length) {
checkUpdateFileIds.value = await checkFileIsUpdateRequest(fileIds); checkUpdateFileIds.value = await checkFileIsUpdateRequest(fileIds);
@ -652,11 +655,11 @@
type: item.type, type: item.type,
name: item.id, name: item.id,
label: item.name, label: item.name,
value: isEditOrCopy.value ? JSON.parse(rule.value) : rule.value, value: rule.value,
options: optionsItem, options: optionsItem,
required: item.required, required: item.required,
props: { props: {
modelValue: isEditOrCopy.value ? JSON.parse(rule.value) : rule.value, modelValue: rule.value,
options: optionsItem, options: optionsItem,
}, },
}; };
@ -707,7 +710,7 @@
try { try {
const res = await downloadFileRequest({ const res = await downloadFileRequest({
projectId: currentProjectId.value, projectId: currentProjectId.value,
caseId: route.query.id as string, caseId: props.caseId,
fileId: item.uid, fileId: item.uid,
local: true, local: true,
}); });

View File

@ -30,7 +30,7 @@
<div class="absolute left-16 top-0 font-normal"> <div class="absolute left-16 top-0 font-normal">
<a-divider direction="vertical" /> <a-divider direction="vertical" />
<a-dropdown :popup-max-height="false" @select="handleSelectType"> <a-dropdown :popup-max-height="false" @select="handleSelectType">
<span class="changeType text-[var(--color-text-3)]" <span class="changeType cursor-pointer text-[var(--color-text-3)]"
>{{ t('system.orgTemplate.changeType') }} <icon-down >{{ t('system.orgTemplate.changeType') }} <icon-down
/></span> /></span>
<template #content> <template #content>
@ -555,8 +555,8 @@
watch( watch(
() => props.form, () => props.form,
() => { (val) => {
detailForm.value = { ...props.form }; detailForm.value = { ...val };
getDetails(); getDetails();
}, },
{ {

View File

@ -1,16 +1,20 @@
<template> <template>
<MsCard :min-width="1100" has-breadcrumb hide-footer no-content-padding hide-divider> <MsCard :loading="loading" :min-width="1100" has-breadcrumb hide-footer no-content-padding hide-divider>
<template #headerLeft> <template #headerLeft>
<a-tooltip :content="reviewName"> <a-tooltip :content="reviewDetail.name">
<div class="one-line-text mr-[8px] max-w-[260px] font-medium text-[var(--color-text-000)]"> <div class="one-line-text mr-[8px] max-w-[260px] font-medium text-[var(--color-text-000)]">
{{ reviewName }} {{ reviewDetail.name }}
</div> </div>
</a-tooltip> </a-tooltip>
<div <div
class="rounded-[0_999px_999px_0] border border-solid border-[text-[rgb(var(--primary-5))]] px-[8px] py-[2px] text-[12px] leading-[16px] text-[rgb(var(--primary-5))]" class="rounded-[0_999px_999px_0] border border-solid border-[text-[rgb(var(--primary-5))]] px-[8px] py-[2px] text-[12px] leading-[16px] text-[rgb(var(--primary-5))]"
> >
<MsIcon type="icon-icon-contacts" size="13" /> <MsIcon type="icon-icon-contacts" size="13" />
{{ t('caseManagement.caseReview.single') }} {{
reviewDetail.reviewPassRule === 'SINGLE'
? t('caseManagement.caseReview.single')
: t('caseManagement.caseReview.multi')
}}
</div> </div>
<div class="ml-[16px] flex items-center"> <div class="ml-[16px] flex items-center">
<a-switch v-model:model-value="onlyMine" size="small" class="mr-[8px]" /> <a-switch v-model:model-value="onlyMine" size="small" class="mr-[8px]" />
@ -22,83 +26,108 @@
<div class="flex h-[calc(100%-1px)] w-full"> <div class="flex h-[calc(100%-1px)] w-full">
<div class="h-full w-[356px] border-r border-[var(--color-text-n8)] pr-[16px] pt-[16px]"> <div class="h-full w-[356px] border-r border-[var(--color-text-n8)] pr-[16px] pt-[16px]">
<div class="mb-[16px] flex"> <div class="mb-[16px] flex">
<a-input <a-input-search
v-model:model-value="keyword" v-model:model-value="keyword"
:placeholder="t('caseManagement.caseReview.searchPlaceholder')" :placeholder="t('caseManagement.caseReview.searchPlaceholder')"
allow-clear allow-clear
class="mr-[8px] w-[240px]" class="mr-[8px] w-[240px]"
@search="loadCaseList"
@press-enter="loadCaseList"
/> />
<a-select v-model:model-value="type" :options="typeOptions" class="w-[92px]"></a-select> <a-select v-model:model-value="type" :options="typeOptions" class="w-[92px]" @change="loadCaseList">
</a-select>
</div> </div>
<div class="case-list"> <a-spin :loading="caseListLoading" class="w-full">
<div <div class="case-list">
v-for="item of caseList" <div
:key="item.id" v-for="item of caseList"
:class="['case-item', activeCase.id === item.id ? 'case-item--active' : '']" :key="item.caseId"
@click="changeActiveCase(item)" :class="['case-item', caseDetail.id === item.caseId ? 'case-item--active' : '']"
> @click="changeActiveCase(item)"
<div class="mb-[4px] flex items-center justify-between"> >
<div>{{ item.id }}</div> <div class="mb-[4px] flex items-center justify-between">
<div class="flex items-center gap-[4px] leading-[22px]"> <div>{{ item.num }}</div>
<MsIcon <div class="flex items-center gap-[4px] leading-[22px]">
:type="resultMap[item.result as ResultMap].icon" <MsIcon
:style="{color: resultMap[item.result as ResultMap].color}" :type="reviewResultMap[item.status]?.icon"
/> :style="{ color: reviewResultMap[item.status]?.color }"
{{ t(resultMap[item.result as ResultMap].label) }} />
{{ t(reviewResultMap[item.status]?.label) }}
</div>
</div> </div>
<a-tooltip :content="item.name">
<div class="one-line-text">{{ item.name }}</div>
</a-tooltip>
</div> </div>
<a-tooltip :content="item.name"> <MsEmpty v-if="caseList.length === 0" />
<div class="one-line-text">{{ item.name }}</div>
</a-tooltip>
</div> </div>
</div> <MsPagination
<MsPagination :total="total" :page-size="pageSize" :current="pageCurrent" size="mini" simple /> v-model:page-size="pageNation.pageSize"
v-model:current="pageNation.current"
:total="pageNation.total"
size="mini"
simple
@change="loadCaseList"
@page-size-change="loadCaseList"
/>
</a-spin>
</div> </div>
<div class="relative flex w-[calc(100%-356px)] flex-col"> <div class="relative flex w-[calc(100%-356px)] flex-col">
<div class="pl-[16px] pt-[16px]"> <div class="pl-[16px] pt-[16px]">
<div class="rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[16px]"> <div class="rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[16px]">
<div class="mb-[12px] flex items-center justify-between"> <div class="mb-[12px] flex items-center justify-between">
<a-tooltip :content="activeCase.module"> <a-tooltip :content="`【${caseDetail.num}】${caseDetail.name}`">
<div class="one-line-text cursor-pointer font-medium text-[rgb(var(--primary-5))]"> <div
{{ activeCase.id }}{{ activeCase.name }} class="one-line-text cursor-pointer font-medium text-[rgb(var(--primary-5))]"
@click="goCaseDetail"
>
{{ caseDetail.num }}{{ caseDetail.name }}
</div> </div>
</a-tooltip> </a-tooltip>
<a-button type="outline" size="mini" class="arco-btn-outline--secondary"> <a-button
type="outline"
size="mini"
class="arco-btn-outline--secondary"
@click="editCaseVisible = true"
>
{{ t('common.edit') }} {{ t('common.edit') }}
</a-button> </a-button>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<MsIcon type="icon-icon_folder_filled1" class="mr-[4px] text-[var(--color-text-4)]" /> <MsIcon type="icon-icon_folder_filled1" class="mr-[4px] text-[var(--color-text-4)]" />
<a-tooltip :content="activeCase.module"> <a-tooltip :content="caseDetail.moduleName || t('common.root')">
<div class="one-line-text mr-[8px] max-w-[260px] font-medium text-[var(--color-text-000)]"> <div class="one-line-text mr-[8px] max-w-[260px] font-medium text-[var(--color-text-000)]">
{{ activeCase.module }} {{ caseDetail.moduleName || t('common.root') }}
</div> </div>
</a-tooltip> </a-tooltip>
<div class="case-detail-label"> <div class="case-detail-label">
{{ t('caseManagement.caseReview.caseLevel') }} {{ t('caseManagement.caseReview.caseLevel') }}
</div> </div>
<div class="case-detail-value"> <div class="case-detail-value">
<caseLevel :case-level="activeCase.level" /> <caseLevel :case-level="caseDetailLevel" />
</div> </div>
<div class="case-detail-label"> <div class="case-detail-label">
{{ t('caseManagement.caseReview.caseVersion') }} {{ t('caseManagement.caseReview.caseVersion') }}
</div> </div>
<div class="case-detail-value"> <div class="case-detail-value">
<MsIcon type="icon-icon_version" size="13" class="mr-[4px]" /> <MsIcon type="icon-icon_version" size="13" class="mr-[4px]" />
{{ activeCase.version }} {{ caseDetail.versionName }}
</div> </div>
<div class="case-detail-label"> <div class="case-detail-label">
{{ t('caseManagement.caseReview.reviewResult') }} {{ t('caseManagement.caseReview.reviewResult') }}
</div> </div>
<div class="case-detail-value"> <div class="case-detail-value">
<div class="flex items-center gap-[4px]"> <div
v-if="reviewResultMap[activeCaseReviewStatus as ReviewResult]"
class="flex items-center gap-[4px]"
>
<MsIcon <MsIcon
:type="resultMap[activeCase.result].icon" :type="reviewResultMap[activeCaseReviewStatus as ReviewResult].icon"
:style="{ :style="{
color: resultMap[activeCase.result].color, color: reviewResultMap[activeCaseReviewStatus as ReviewResult].color,
}" }"
/> />
{{ t(resultMap[activeCase.result].label) }} {{ t(reviewResultMap[activeCaseReviewStatus as ReviewResult].label) }}
</div> </div>
</div> </div>
</div> </div>
@ -128,41 +157,49 @@
<div v-else-if="showTab === 'detail'" class="h-full"> <div v-else-if="showTab === 'detail'" class="h-full">
<MsSplitBox :size="0.8" direction="vertical" min="0" :max="0.99"> <MsSplitBox :size="0.8" direction="vertical" min="0" :max="0.99">
<template #first> <template #first>
<caseTabDetail :form="detailForm" :allow-edit="false" /> <caseTabDetail :form="caseDetail" :allow-edit="false" />
</template> </template>
<template #second> <template #second>
<div class="flex h-full flex-col overflow-hidden"> <div class="flex h-full flex-col overflow-hidden">
<div class="mb-[8px] font-medium text-[var(--color-text-1)]"> <div class="my-[8px] font-medium text-[var(--color-text-1)]">
{{ t('caseManagement.caseReview.reviewHistory') }} {{ t('caseManagement.caseReview.reviewHistory') }}
</div> </div>
<div class="review-history-list"> <div class="review-history-list">
<div v-for="item of reviewHistoryList" :key="item.id" class="mb-[16px]"> <a-spin :loading="reviewHistoryListLoading" class="h-full w-full">
<div class="flex items-center"> <div v-for="item of reviewHistoryList" :key="item.id" class="mb-[16px]">
<a-avatar>A</a-avatar> <div class="flex items-center">
<div class="ml-[8px] flex items-center"> <MSAvatar :avatar="item.userLogo" />
<div class="font-medium text-[var(--color-text-1)]">{{ item.reviewer }}</div> <div class="ml-[8px] flex items-center">
<a-divider direction="vertical" margin="8px"></a-divider> <div class="font-medium text-[var(--color-text-1)]">{{ item.userName }}</div>
<div v-if="item.result === 1" class="flex items-center"> <a-divider direction="vertical" margin="8px"></a-divider>
<MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--success-6))]" /> <div v-if="item.status === 'PASS'" class="flex items-center">
{{ t('caseManagement.caseReview.pass') }} <MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--success-6))]" />
</div> {{ t('caseManagement.caseReview.pass') }}
<div v-else-if="item.result === 2" class="flex items-center"> </div>
<MsIcon type="icon-icon_close_filled" class="mr-[4px] text-[rgb(var(--danger-6))]" /> <div v-else-if="item.status === 'UN_PASS'" class="flex items-center">
{{ t('caseManagement.caseReview.fail') }} <MsIcon type="icon-icon_close_filled" class="mr-[4px] text-[rgb(var(--danger-6))]" />
</div> {{ t('caseManagement.caseReview.fail') }}
<div v-else-if="item.result === 3" class="flex items-center"> </div>
<MsIcon type="icon-icon_warning_filled" class="mr-[4px] text-[rgb(var(--warning-6))]" /> <div v-else-if="item.status === 'UNDER_REVIEWED'" class="flex items-center">
{{ t('caseManagement.caseReview.suggestion') }} <MsIcon type="icon-icon_warning_filled" class="mr-[4px] text-[rgb(var(--warning-6))]" />
</div> {{ t('caseManagement.caseReview.suggestion') }}
<div v-else-if="item.result === 4" class="flex items-center"> </div>
<MsIcon type="icon-icon_resubmit_filled" class="mr-[4px] text-[rgb(var(--warning-6))]" /> <div v-else-if="item.status === 'RE_REVIEWED'" class="flex items-center">
{{ t('caseManagement.caseReview.reReview') }} <MsIcon
type="icon-icon_resubmit_filled"
class="mr-[4px] text-[rgb(var(--warning-6))]"
/>
{{ t('caseManagement.caseReview.reReview') }}
</div>
</div> </div>
</div> </div>
<div class="ml-[48px] text-[var(--color-text-2)]" v-html="item.contentText"></div>
<div class="ml-[48px] mt-[8px] text-[var(--color-text-4)]">
{{ dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss') }}
</div>
</div> </div>
<div class="ml-[48px] text-[var(--color-text-2)]">{{ item.reason }}</div> <MsEmpty v-if="reviewHistoryList.length === 0" />
<div class="ml-[48px] mt-[8px] text-[var(--color-text-4)]">{{ item.time }}</div> </a-spin>
</div>
</div> </div>
</div> </div>
</template> </template>
@ -182,7 +219,7 @@
</div> </div>
<caseTabDemand <caseTabDemand
ref="caseDemandRef" ref="caseDemandRef"
:fun-params="{ caseId: route.query.id as string, keyword: demandKeyword }" :fun-params="{ caseId: route.query.caseId as string, keyword: demandKeyword }"
/> />
</div> </div>
</div> </div>
@ -207,19 +244,19 @@
<a-form ref="dialogFormRef" :model="caseResultForm" layout="vertical"> <a-form ref="dialogFormRef" :model="caseResultForm" layout="vertical">
<a-form-item field="reason" :label="t('caseManagement.caseReview.reviewResult')" class="mb-[8px]"> <a-form-item field="reason" :label="t('caseManagement.caseReview.reviewResult')" class="mb-[8px]">
<a-radio-group v-model:model-value="caseResultForm.result" @change="() => dialogFormRef?.resetFields()"> <a-radio-group v-model:model-value="caseResultForm.result" @change="() => dialogFormRef?.resetFields()">
<a-radio value="pass"> <a-radio value="PASS">
<div class="inline-flex items-center"> <div class="inline-flex items-center">
<MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--success-6))]" /> <MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--success-6))]" />
{{ t('caseManagement.caseReview.pass') }} {{ t('caseManagement.caseReview.pass') }}
</div> </div>
</a-radio> </a-radio>
<a-radio value="fail"> <a-radio value="UN_PASS">
<div class="inline-flex items-center"> <div class="inline-flex items-center">
<MsIcon type="icon-icon_close_filled" class="mr-[4px] text-[rgb(var(--danger-6))]" /> <MsIcon type="icon-icon_close_filled" class="mr-[4px] text-[rgb(var(--danger-6))]" />
{{ t('caseManagement.caseReview.fail') }} {{ t('caseManagement.caseReview.fail') }}
</div> </div>
</a-radio> </a-radio>
<a-radio value="suggestion"> <a-radio value="UNDER_REVIEWED">
<div class="inline-flex items-center"> <div class="inline-flex items-center">
<MsIcon type="icon-icon_warning_filled" class="mr-[4px] text-[rgb(var(--warning-6))]" /> <MsIcon type="icon-icon_warning_filled" class="mr-[4px] text-[rgb(var(--warning-6))]" />
{{ t('caseManagement.caseReview.suggestion') }} {{ t('caseManagement.caseReview.suggestion') }}
@ -237,20 +274,17 @@
field="reason" field="reason"
:label="t('caseManagement.caseReview.reason')" :label="t('caseManagement.caseReview.reason')"
:rules=" :rules="
caseResultForm.result === 'fail' caseResultForm.result === 'UN_PASS'
? [{ required: true, message: t('caseManagement.caseReview.reasonRequired') }] ? [{ required: true, message: t('caseManagement.caseReview.reasonRequired') }]
: [] : []
" "
asterisk-position="end" asterisk-position="end"
class="mb-0" class="mb-0"
> >
<a-input <MsRichText v-model:modelValue="caseResultForm.reason" class="w-full" />
v-model:model-value="caseResultForm.reason"
:placeholder="t('caseManagement.caseReview.reasonPlaceholder')"
/>
</a-form-item> </a-form-item>
</a-form> </a-form>
<a-button type="primary" class="mt-[16px]"> <a-button type="primary" class="mt-[16px]" :loading="submitReviewLoading" @click="submitReview">
{{ t('caseManagement.caseReview.submitReview') }} {{ t('caseManagement.caseReview.submitReview') }}
</a-button> </a-button>
</div> </div>
@ -258,73 +292,136 @@
</div> </div>
</div> </div>
</MsCard> </MsCard>
<MsDrawer
v-model:visible="editCaseVisible"
:title="t('caseManagement.caseReview.updateCase')"
:width="1200"
:ok-text="t('common.update')"
:ok-loading="updateCaseLoading"
@confirm="updateCase"
>
<caseTemplateDetail v-if="editCaseVisible" v-model:form-mode-value="editCaseForm" :case-id="activeCaseId" />
</MsDrawer>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
/** /**
* @description 功能测试-用例评审-用例详情 * @description 功能测试-用例评审-用例详情
*/ */
import { useRoute } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { FormInstance } from '@arco-design/web-vue'; import { FormInstance, Message } from '@arco-design/web-vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import MSAvatar from '@/components/pure/ms-avatar/index.vue';
import MsCard from '@/components/pure/ms-card/index.vue'; import MsCard from '@/components/pure/ms-card/index.vue';
import MsDescription from '@/components/pure/ms-description/index.vue'; import MsDescription, { Description } from '@/components/pure/ms-description/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsEmpty from '@/components/pure/ms-empty/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue'; import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsPagination from '@/components/pure/ms-pagination/index'; import MsPagination from '@/components/pure/ms-pagination/index';
import MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue'; import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue'; import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import type { CaseLevel } from '@/components/business/ms-case-associate/types'; import type { CaseLevel } from '@/components/business/ms-case-associate/types';
import caseTemplateDetail from '../caseManagementFeature/components/caseTemplateDetail.vue';
import caseTabDemand from '../caseManagementFeature/components/tabContent/tabDemand/associatedDemandTable.vue'; import caseTabDemand from '../caseManagementFeature/components/tabContent/tabDemand/associatedDemandTable.vue';
import caseTabDetail from '../caseManagementFeature/components/tabContent/tabDetail.vue'; import caseTabDetail from '../caseManagementFeature/components/tabContent/tabDetail.vue';
import {
getCaseReviewHistoryList,
getReviewDetail,
getReviewDetailCasePage,
saveCaseReviewResult,
} from '@/api/modules/case-management/caseReview';
import { getCaseDetail } from '@/api/modules/case-management/featureCase';
import { reviewDefaultDetail, reviewResultMap } from '@/config/apiTest';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { ReviewCaseItem, ReviewHistoryItem, ReviewItem, ReviewResult } from '@/models/caseManagement/caseReview';
import type { DetailCase } from '@/models/caseManagement/featureCase'; import type { DetailCase } from '@/models/caseManagement/featureCase';
import { CaseManagementRouteEnum } from '@/enums/routeEnum';
const route = useRoute(); const route = useRoute();
const router = useRouter();
const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const reviewName = ref('打算肯定还是觉得还是觉得还是计划的'); const reviewDetail = ref<ReviewItem>({ ...reviewDefaultDetail });
const caseDetail = ref({ const loading = ref(false);
demandCount: 999,
}); //
const onlyMine = ref(false); async function initDetail() {
const keyword = ref(''); try {
loading.value = true;
const res = await getReviewDetail(route.query.id as string);
reviewDetail.value = res;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
type ResultMap = 0 | 1 | 2 | 3;
const resultMap = {
0: {
label: t('caseManagement.caseReview.unReview'),
color: 'var(--color-text-input-border)',
icon: 'icon-icon_block_filled',
},
1: {
label: t('caseManagement.caseReview.reviewPass'),
color: 'rgb(var(--success-6))',
icon: 'icon-icon_succeed_filled',
},
2: {
label: t('caseManagement.caseReview.fail'),
color: 'rgb(var(--danger-6))',
icon: 'icon-icon_close_filled',
},
3: {
label: t('caseManagement.caseReview.reReview'),
color: 'rgb(var(--warning-6))',
icon: 'icon-icon_resubmit_filled',
},
} as const;
const type = ref(''); const type = ref('');
const typeOptions = ref([ const typeOptions = ref([
{ label: '全部', value: '' }, { label: t('common.all'), value: '' },
{ label: resultMap[0].label, value: 'unReview' }, { label: t(reviewResultMap.UN_REVIEWED.label), value: 'UN_REVIEWED' },
{ label: resultMap[1].label, value: 'reviewPass' }, { label: t(reviewResultMap.PASS.label), value: 'PASS' },
{ label: resultMap[2].label, value: 'fail' }, { label: t(reviewResultMap.UN_PASS.label), value: 'UN_PASS' },
{ label: resultMap[3].label, value: 'reReview' }, { label: t(reviewResultMap.RE_REVIEWED.label), value: 'RE_REVIEWED' },
]); ]);
const initDetail: DetailCase = { const onlyMine = ref(false);
const keyword = ref('');
const caseList = ref<ReviewCaseItem[]>([]);
const pageNation = ref({
total: 0,
pageSize: 10,
current: 1,
});
const otherListQueryParams = ref<Record<string, any>>({});
const caseListLoading = ref(false);
//
async function loadCaseList() {
try {
caseListLoading.value = true;
const res = await getReviewDetailCasePage({
projectId: appStore.currentProjectId,
reviewId: route.query.id as string,
viewFlag: onlyMine.value,
keyword: keyword.value,
current: pageNation.value.current,
pageSize: pageNation.value.pageSize,
filter: type.value
? {
status: [type.value],
}
: undefined,
...otherListQueryParams.value,
});
caseList.value = res.list;
pageNation.value.total = res.total;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
caseListLoading.value = false;
}
}
watch(
() => onlyMine.value,
() => loadCaseList()
);
const activeCaseId = ref(route.query.caseId as string);
const activeCaseReviewStatus = computed(() => {
return caseList.value.find((e) => e.caseId === activeCaseId.value)?.status;
});
const defaultCaseDetail: DetailCase = {
id: '', id: '',
projectId: '', projectId: '',
templateId: '', templateId: '',
@ -341,60 +438,57 @@
tags: [], tags: [],
customFields: [], // customFields: [], //
relateFileMetaIds: [], // ID relateFileMetaIds: [], // ID
reviewStatus: 'UN_REVIEWED',
functionalPriority: '',
}; };
const detailForm = ref<DetailCase>({ ...initDetail }); const caseDetail = ref<DetailCase>({ ...defaultCaseDetail });
const descriptions = ref<Description[]>([]);
const caseList = ref([ const caseDetailLevel = computed<CaseLevel>(() => {
{ if (caseDetail.value.functionalPriority) {
id: 'g4ggtrgrtg', return (Number(JSON.parse(caseDetail.value.functionalPriority).match(/\d+/g)[0]) as CaseLevel) || 0; //
name: '打算肯定还是觉得还是觉得还是计划的', }
module: '模块名称模块名称模块名称模块名称模块名称模块名称模块名称模块名称模块名称', return 0;
level: 0 as CaseLevel,
result: 0 as ResultMap,
version: '1.0.0',
},
{
id: 2,
name: '打算肯定还是觉得还是觉得还是计划的',
result: 1,
module: '模块名称模块名称模块名称模块名称模块名称模块名称模块名称模块名称模块名称',
level: 0 as CaseLevel,
version: '1.0.0',
},
{
id: 3,
name: '打算肯定还是觉得还是觉得还是计划的',
result: 2,
module: '模块名称模块名称模块名称模块名称模块名称模块名称模块名称模块名称模块名称',
level: 0 as CaseLevel,
version: '1.0.0',
},
{
id: 4,
name: '打算肯定还是觉得还是觉得还是计划的',
result: 3,
module: '模块名称模块名称模块名称模块名称模块名称模块名称模块名称模块名称模块名称',
level: 0 as CaseLevel,
version: '1.0.0',
},
]);
const total = ref(10);
const pageSize = ref(10);
const pageCurrent = ref(1);
const activeCase = ref({
id: 'g4ggtrgrtg',
name: '打算肯定还是觉得还是觉得还是计划的打撒打扫打扫',
module: '模块名称模块名称模块名称模块名称模块名称模块名称模块名称模块名称模块名称',
level: 0 as CaseLevel,
result: 0 as ResultMap,
version: '1.0.0',
}); });
function changeActiveCase(item: any) { function changeActiveCase(item: ReviewCaseItem) {
if (activeCase.value.id !== item.id) { if (activeCaseId.value !== item.caseId) {
activeCase.value = item; activeCaseId.value = item.caseId;
} }
} }
//
async function loadCaseDetail() {
try {
const res = await getCaseDetail(activeCaseId.value);
caseDetail.value = res;
descriptions.value = [
{
label: t('caseManagement.caseReview.belongModule'),
value: res.moduleName || t('common.root'),
},
//
...res.customFields.map((e) => {
const val =
typeof e.defaultValue === 'string' && e.defaultValue !== '' ? JSON.parse(e.defaultValue) : e.defaultValue;
return {
label: e.fieldName,
value: Array.isArray(val) ? val.join('、') : val,
};
}),
{
label: t('caseManagement.caseReview.creator'),
value: res.createUser || '',
},
{
label: t('caseManagement.caseReview.createTime'),
value: dayjs().format('YYYY-MM-DD HH:mm:ss'),
},
];
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
const showTab = ref('detail'); const showTab = ref('detail');
const tabList = ref([ const tabList = ref([
{ {
@ -411,67 +505,36 @@
}, },
]); ]);
const descriptions = ref([ const reviewHistoryListLoading = ref(false);
{ const reviewHistoryList = ref<ReviewHistoryItem[]>([]);
label: t('caseManagement.caseReview.belongModule'),
value: '模块模块',
},
{
label: t('caseManagement.caseReview.caseStatus'),
value: '未开始',
},
{
label: t('caseManagement.caseReview.responsiblePerson'),
value: '张三',
},
{
label: t('caseManagement.caseReview.creator'),
value: '李四',
},
{
label: t('caseManagement.caseReview.createTime'),
value: dayjs().format('YYYY-MM-DD HH:mm:ss'),
},
]);
const reviewHistoryList = ref([ //
{ async function initReviewHistoryList() {
id: 1, try {
reviewer: '张三', reviewHistoryListLoading.value = true;
avatar: '', const res = await getCaseReviewHistoryList(route.query.id as string, activeCaseId.value);
result: 1, reviewHistoryList.value = res;
reason: '', } catch (error) {
time: dayjs().format('YYYY-MM-DD HH:mm:ss'), // eslint-disable-next-line no-console
}, console.log(error);
{ } finally {
id: 2, reviewHistoryListLoading.value = false;
reviewer: '李四', }
avatar: '', }
result: 2,
reason: '不通过', watch(
time: dayjs().format('YYYY-MM-DD HH:mm:ss'), () => activeCaseId.value,
}, () => {
{ loadCaseDetail();
id: 3, if (showTab.value === 'detail') {
reviewer: '王五', initReviewHistoryList();
avatar: '', }
result: 3, }
reason: '建议修改', );
time: dayjs().format('YYYY-MM-DD HH:mm:ss'),
},
{
id: 4,
reviewer: '李六',
avatar: '',
result: 4,
reason: '重新提',
time: dayjs().format('YYYY-MM-DD HH:mm:ss'),
},
]);
const autoNext = ref(false); const autoNext = ref(false);
const caseResultForm = ref({ const caseResultForm = ref({
result: 'pass', result: 'PASS' as ReviewResult,
reason: '', reason: '',
}); });
const dialogFormRef = ref<FormInstance>(); const dialogFormRef = ref<FormInstance>();
@ -481,6 +544,110 @@
function searchDemand() { function searchDemand() {
caseDemandRef.value?.initData(); caseDemandRef.value?.initData();
} }
function goCaseDetail() {
router.push({
name: CaseManagementRouteEnum.CASE_MANAGEMENT,
query: {
id: activeCaseId.value,
},
});
}
const submitReviewLoading = ref(false);
//
function submitReview() {
dialogFormRef.value?.validate(async (errors) => {
if (!errors) {
try {
submitReviewLoading.value = true;
const params = {
projectId: appStore.currentProjectId,
caseId: activeCaseId.value,
reviewId: route.query.id as string,
status: caseResultForm.value.result,
reviewPassRule: reviewDetail.value.reviewPassRule,
content: caseResultForm.value.reason,
notifier: '', // TODO:
};
await saveCaseReviewResult(params);
Message.success(t('caseManagement.caseReview.reviewSuccess'));
caseResultForm.value = {
result: 'PASS' as ReviewResult,
reason: '',
};
if (autoNext.value) {
const index = caseList.value.findIndex((e) => e.caseId === activeCaseId.value);
if (index < caseList.value.length - 1) {
activeCaseId.value = caseList.value[index + 1].caseId;
}
}
loadCaseList();
loadCaseDetail();
initReviewHistoryList();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
submitReviewLoading.value = false;
}
}
});
}
const editCaseVisible = ref(false);
const editCaseForm = ref<Record<string, any>>({});
const updateCaseLoading = ref(false);
async function updateCase() {
try {
updateCaseLoading.value = true;
// await updateCaseRequest();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
updateCaseLoading.value = false;
}
}
onBeforeMount(() => {
const lastPageParams = window.history.state.params ? JSON.parse(window.history.state.params) : null; //
if (lastPageParams) {
const {
total,
pageSize,
current,
onlyMine: _onlyMine,
keyword: _keyword,
combine,
sort,
searchMode,
moduleIds,
} = lastPageParams;
pageNation.value = {
total,
pageSize,
current,
};
onlyMine.value = !!_onlyMine;
keyword.value = _keyword;
otherListQueryParams.value = {
combine,
sort,
searchMode,
moduleIds,
};
} else {
keyword.value = route.query.caseId as string;
}
initDetail();
loadCaseList();
loadCaseDetail();
if (showTab.value === 'detail') {
initReviewHistoryList();
}
});
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@ -493,8 +660,10 @@
background: var(--color-text-n9); background: var(--color-text-n9);
.case-item { .case-item {
@apply cursor-pointer; @apply cursor-pointer;
&:not(:last-child) {
margin-bottom: 8px;
}
margin-bottom: 8px;
padding: 16px; padding: 16px;
border-radius: var(--border-radius-small); border-radius: var(--border-radius-small);
background-color: white; background-color: white;

View File

@ -5,21 +5,18 @@
v-model:currentSelectCase="currentSelectCase" v-model:currentSelectCase="currentSelectCase"
:ok-button-disabled="associateForm.reviewers.length === 0" :ok-button-disabled="associateForm.reviewers.length === 0"
:get-modules-func="getCaseModuleTree" :get-modules-func="getCaseModuleTree"
:modules-params="modulesTreeParams"
:get-table-func="getCaseList" :get-table-func="getCaseList"
:modules-count="modulesCount"
:confirm-loading="confirmLoading" :confirm-loading="confirmLoading"
:associated-ids="associatedIds" :associated-ids="associatedIds"
@close="emit('close')" @close="emit('close')"
@save="saveHandler" @save="saveHandler"
@init="getModuleCount"
> >
<template #footerLeft> <template #footerLeft>
<a-form ref="associateFormRef" :model="associateForm"> <a-form ref="associateFormRef" :model="associateForm">
<a-form-item <a-form-item
field="reviewers" field="reviewers"
:rules="[{ required: true, message: t('caseManagement.caseReview.reviewerRequired') }]" :rules="[{ required: true, message: t('caseManagement.caseReview.reviewerRequired') }]"
class="mb-0" class="review-item mb-0"
> >
<template #label> <template #label>
<div class="inline-flex items-center"> <div class="inline-flex items-center">
@ -52,7 +49,7 @@
allow-search allow-search
allow-clear allow-clear
multiple multiple
class="w-[300px]" class="w-[290px]"
:loading="reviewerLoading" :loading="reviewerLoading"
> >
<template #empty> <template #empty>
@ -73,19 +70,19 @@
<script setup lang="ts"> <script setup lang="ts">
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useVModel } from '@vueuse/core';
import { FormInstance, SelectOptionData } from '@arco-design/web-vue'; import { FormInstance, SelectOptionData } from '@arco-design/web-vue';
import MsCaseAssociate from '@/components/business/ms-case-associate/index.vue'; import MsCaseAssociate from '@/components/business/ms-case-associate/index.vue';
import MsSelect from '@/components/business/ms-select'; import MsSelect from '@/components/business/ms-select';
import { getReviewUsers } from '@/api/modules/case-management/caseReview'; import { getReviewUsers } from '@/api/modules/case-management/caseReview';
import { getCaseList, getCaseModulesCounts, getCaseModuleTree } from '@/api/modules/case-management/featureCase'; import { getCaseList, getCaseModuleTree } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useLocale from '@/locale/useLocale'; import useLocale from '@/locale/useLocale';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import type { CaseModuleQueryParams } from '@/models/caseManagement/featureCase'; import { BaseAssociateCaseRequest } from '@/models/caseManagement/caseReview';
import type { TableQueryParams } from '@/models/common';
import { ProjectManagementRouteEnum } from '@/enums/routeEnum'; import { ProjectManagementRouteEnum } from '@/enums/routeEnum';
const props = defineProps<{ const props = defineProps<{
@ -95,7 +92,7 @@
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:visible', val: boolean): void; (e: 'update:visible', val: boolean): void;
(e: 'update:project', val: string): void; (e: 'update:project', val: string): void;
(e: 'success', val: string[]): void; (e: 'success', val: BaseAssociateCaseRequest & { reviewers: string[] }): void;
(e: 'close'): void; (e: 'close'): void;
}>(); }>();
const router = useRouter(); const router = useRouter();
@ -103,42 +100,11 @@
const { currentLocale } = useLocale(); const { currentLocale } = useLocale();
const { t } = useI18n(); const { t } = useI18n();
const innerVisible = ref(false); const innerVisible = useVModel(props, 'visible', emit);
const innerProject = useVModel(props, 'project', emit);
watch(
() => props.visible,
(val) => {
innerVisible.value = val;
}
);
watch(
() => innerVisible.value,
(val) => {
if (!val) {
emit('update:visible', false);
}
}
);
const innerProject = ref(appStore.currentProjectId);
watch(
() => props.project,
(val) => {
innerProject.value = val;
}
);
watch(
() => innerProject.value,
(val) => {
emit('update:project', val);
}
);
const associateForm = ref({ const associateForm = ref({
reviewers: [], reviewers: [] as string[],
}); });
const associateFormRef = ref<FormInstance>(); const associateFormRef = ref<FormInstance>();
@ -167,24 +133,44 @@
} }
const currentSelectCase = ref<string | number | Record<string, any> | undefined>(''); const currentSelectCase = ref<string | number | Record<string, any> | undefined>('');
const modulesCount = ref<Record<string, any>>({});
const modulesTreeParams = ref<TableQueryParams>({});
async function getModuleCount(params: TableQueryParams) {
try {
modulesCount.value = await getCaseModulesCounts(params);
} catch (error) {
console.log(error);
}
}
const associatedIds = ref<string[]>([]); const associatedIds = ref<string[]>([]);
const confirmLoading = ref<boolean>(false); const confirmLoading = ref<boolean>(false);
function saveHandler(params: TableQueryParams) {} function saveHandler(params: BaseAssociateCaseRequest) {
associateFormRef.value?.validate(async (errors) => {
if (!errors) {
try {
confirmLoading.value = true;
associatedIds.value = [...params.selectIds];
emit('success', { ...params, reviewers: associateForm.value.reviewers });
innerVisible.value = false;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
confirmLoading.value = false;
}
}
});
}
onBeforeMount(() => { watch(
initReviewers(); () => props.visible,
}); (val) => {
if (val) {
//
initReviewers();
}
}
);
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped>
:deep(.review-item) {
.arco-form-item-label-col {
flex: none;
width: auto;
}
}
</style>

View File

@ -1,33 +1,28 @@
<template> <template>
<div class="px-[24px] py-[16px]"> <div class="px-[24px] py-[16px]">
<div class="mb-[16px] flex items-center justify-between"> <div class="mb-[16px] flex flex-wrap items-center justify-end">
<div class="flex items-center"> <MsAdvanceFilter
<a-button type="primary" class="mr-[12px]" @click="associateDrawerVisible = true"> v-model:keyword="keyword"
{{ t('ms.case.associate.title') }} :filter-config-list="filterConfigList"
</a-button> :row-count="filterRowCount"
<a-button type="outline" @click="createCase">{{ t('caseManagement.caseReview.createCase') }}</a-button> :search-placeholder="t('caseManagement.caseReview.searchPlaceholder')"
</div> @keyword-search="() => searchCase()"
<div class="flex w-[70%] items-center justify-end gap-[8px]"> @adv-search="searchCase"
<a-input-search @reset="searchCase"
v-model="keyword" >
:placeholder="t('caseManagement.caseReview.searchPlaceholder')" <template #right>
allow-clear <div class="flex items-center">
class="w-[200px]" <a-radio-group v-model:model-value="showType" type="button" class="case-show-type">
@press-enter="searchReview" <a-radio value="list" class="show-type-icon p-[2px]">
@search="searchReview" <MsIcon type="icon-icon_view-list_outlined" />
/> </a-radio>
<a-button type="outline" class="arco-btn-outline--secondary px-[8px]"> <a-radio value="mind" class="show-type-icon p-[2px]">
<MsIcon type="icon-icon-filter" class="mr-[4px] text-[var(--color-text-4)]" /> <MsIcon type="icon-icon_mindnote_outlined" />
<div class="text-[var(--color-text-4)]">{{ t('common.filter') }}</div> </a-radio>
</a-button> </a-radio-group>
<a-radio-group v-model:model-value="showType" type="button" class="case-show-type"> </div>
<a-radio value="list" class="show-type-icon p-[2px]"><MsIcon type="icon-icon_view-list_outlined" /></a-radio> </template>
<a-radio value="mind" class="show-type-icon p-[2px]"><MsIcon type="icon-icon_mindnote_outlined" /></a-radio> </MsAdvanceFilter>
</a-radio-group>
<a-button type="outline" class="arco-btn-outline--secondary p-[10px]">
<icon-refresh class="text-[var(--color-text-4)]" />
</a-button>
</div>
</div> </div>
<ms-base-table <ms-base-table
v-bind="propsRes" v-bind="propsRes"
@ -51,20 +46,25 @@
</template> </template>
<template #name="{ record }"> <template #name="{ record }">
<a-tooltip :content="record.name"> <a-tooltip :content="record.name">
<a-button type="text" class="px-0" @click="openDetail(record.id)"> <a-button type="text" class="px-0" @click="review(record)">
<div class="one-line-text max-w-[168px]">{{ record.name }}</div> <div class="one-line-text max-w-[168px]">{{ record.name }}</div>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
</template> </template>
<template #result="{ record }"> <template #reviewNames="{ record }">
<a-tooltip :content="record.reviewNames.join('、')">
<div class="one-line-text">{{ record.reviewNames.join('、') }}</div>
</a-tooltip>
</template>
<template #status="{ record }">
<div class="flex items-center gap-[4px]"> <div class="flex items-center gap-[4px]">
<MsIcon <MsIcon
:type="resultMap[record.result as ResultMap].icon" :type="reviewResultMap[record.status as ReviewResult].icon"
:style="{ :style="{
color: resultMap[record.result as ResultMap].color color: reviewResultMap[record.status as ReviewResult].color
}" }"
/> />
{{ t(resultMap[record.result as ResultMap].label) }} {{ t(reviewResultMap[record.status as ReviewResult].label) }}
</div> </div>
</template> </template>
<template #action="{ record }"> <template #action="{ record }">
@ -76,7 +76,9 @@
:title="t('caseManagement.caseReview.disassociateTip')" :title="t('caseManagement.caseReview.disassociateTip')"
:sub-title-tip="t('caseManagement.caseReview.disassociateTipContent')" :sub-title-tip="t('caseManagement.caseReview.disassociateTipContent')"
:ok-text="t('common.confirm')" :ok-text="t('common.confirm')"
:loading="disassociateLoading"
type="error" type="error"
@confirm="(val, done) => handleDisassociateReviewCase(record, done)"
> >
<MsButton type="text" class="!mr-0"> <MsButton type="text" class="!mr-0">
{{ t('caseManagement.caseReview.disassociate') }} {{ t('caseManagement.caseReview.disassociate') }}
@ -98,6 +100,7 @@
class="p-[4px]" class="p-[4px]"
title-align="start" title-align="start"
body-class="p-0" body-class="p-0"
:width="['review', 'reReview'].includes(dialogShowType) ? 680 : 480"
:mask-closable="false" :mask-closable="false"
@close="handleDialogCancel" @close="handleDialogCancel"
> >
@ -129,13 +132,13 @@
class="mb-[16px]" class="mb-[16px]"
> >
<a-radio-group v-model:model-value="dialogForm.result" @change="() => dialogFormRef?.resetFields()"> <a-radio-group v-model:model-value="dialogForm.result" @change="() => dialogFormRef?.resetFields()">
<a-radio value="pass"> <a-radio value="PASS">
<div class="inline-flex items-center"> <div class="inline-flex items-center">
<MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--success-6))]" /> <MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--success-6))]" />
{{ t('caseManagement.caseReview.pass') }} {{ t('caseManagement.caseReview.pass') }}
</div> </div>
</a-radio> </a-radio>
<a-radio value="fail"> <a-radio value="UN_PASS">
<div class="inline-flex items-center"> <div class="inline-flex items-center">
<MsIcon type="icon-icon_close_filled" class="mr-[4px] text-[rgb(var(--danger-6))]" /> <MsIcon type="icon-icon_close_filled" class="mr-[4px] text-[rgb(var(--danger-6))]" />
{{ t('caseManagement.caseReview.fail') }} {{ t('caseManagement.caseReview.fail') }}
@ -155,10 +158,11 @@
asterisk-position="end" asterisk-position="end"
class="mb-0" class="mb-0"
> >
<a-input <!-- <a-input
v-model:model-value="dialogForm.reason" v-model:model-value="dialogForm.reason"
:placeholder="t('caseManagement.caseReview.reasonPlaceholder')" :placeholder="t('caseManagement.caseReview.reasonPlaceholder')"
/> /> -->
<MsRichText v-model:modelValue="dialogForm.reason" class="w-full" />
</a-form-item> </a-form-item>
<a-form-item <a-form-item
v-if="dialogShowType === 'changeReviewer'" v-if="dialogShowType === 'changeReviewer'"
@ -171,6 +175,7 @@
<MsSelect <MsSelect
v-model:modelValue="dialogForm.reviewer" v-model:modelValue="dialogForm.reviewer"
mode="static" mode="static"
:loading="reviewerLoading"
:placeholder="t('caseManagement.caseReview.reviewerPlaceholder')" :placeholder="t('caseManagement.caseReview.reviewerPlaceholder')"
:options="reviewersOptions" :options="reviewersOptions"
:search-keys="['label']" :search-keys="['label']"
@ -195,94 +200,104 @@
/> />
</a-tooltip> </a-tooltip>
</div> </div>
<a-button type="secondary" @click="handleDialogCancel">{{ t('common.cancel') }}</a-button> <a-button type="secondary" :disabled="dialogLoading" @click="handleDialogCancel">
<a-button v-if="dialogShowType === 'review'" type="primary" class="ml-[12px]" @click="commitResult"> {{ t('common.cancel') }}
</a-button>
<a-button
v-if="dialogShowType === 'review'"
type="primary"
class="ml-[12px]"
:loading="dialogLoading"
@click="commitResult"
>
{{ t('caseManagement.caseReview.commitResult') }} {{ t('caseManagement.caseReview.commitResult') }}
</a-button> </a-button>
<a-button v-if="dialogShowType === 'changeReviewer'" type="primary" class="ml-[12px]" @click="changeReviewer"> <a-button
v-if="dialogShowType === 'changeReviewer'"
type="primary"
class="ml-[12px]"
:loading="dialogLoading"
@click="changeReviewer"
>
{{ t('common.update') }} {{ t('common.update') }}
</a-button> </a-button>
<a-button v-if="dialogShowType === 'reReview'" type="primary" class="ml-[12px]" @click="reReview"> <a-button
v-if="dialogShowType === 'reReview'"
type="primary"
class="ml-[12px]"
:loading="dialogLoading"
@click="reReview"
>
{{ t('caseManagement.caseReview.reReview') }} {{ t('caseManagement.caseReview.reReview') }}
</a-button> </a-button>
</div> </div>
</template> </template>
</a-modal> </a-modal>
<AssociateDrawer
v-model:visible="associateDrawerVisible"
v-model:project="associateDrawerProject"
@success="writeAssociateCases"
/>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { FormInstance, Message } from '@arco-design/web-vue'; import { FormInstance, Message, SelectOptionData } from '@arco-design/web-vue';
import { MsAdvanceFilter } from '@/components/pure/ms-advance-filter';
import { FilterFormItem, FilterResult, FilterType } from '@/components/pure/ms-advance-filter/type';
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue'; import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsPopconfirm from '@/components/pure/ms-popconfirm/index.vue'; import MsPopconfirm from '@/components/pure/ms-popconfirm/index.vue';
import MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue'; import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type'; import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable'; import useTable from '@/components/pure/ms-table/useTable';
import MsSelect from '@/components/business/ms-select'; import MsSelect from '@/components/business/ms-select';
import AssociateDrawer from '../create/associateDrawer.vue';
import { getReviewList } from '@/api/modules/case-management/caseReview'; import {
batchChangeReviewer,
batchDisassociateReviewCase,
batchReview,
disassociateReviewCase,
getReviewDetailCasePage,
getReviewUsers,
} from '@/api/modules/case-management/caseReview';
import { reviewResultMap, reviewStatusMap } from '@/config/apiTest';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal'; import useModal from '@/hooks/useModal';
import useTableStore from '@/hooks/useTableStore'; import useTableStore from '@/hooks/useTableStore';
import useAppStore from '@/store/modules/app';
import useUserStore from '@/store/modules/user';
import { ReviewCaseItem, ReviewItem, ReviewPassRule, ReviewResult } from '@/models/caseManagement/caseReview';
import { BatchApiParams } from '@/models/common';
import type { ModuleTreeNode } from '@/models/projectManagement/file';
import { CaseManagementRouteEnum } from '@/enums/routeEnum'; import { CaseManagementRouteEnum } from '@/enums/routeEnum';
import { TableKeyEnum } from '@/enums/tableEnum'; import { TableKeyEnum } from '@/enums/tableEnum';
const props = defineProps<{ const props = defineProps<{
activeFolder: string | number; activeFolder: string | number;
onlyMine: boolean;
reviewPassRule: ReviewPassRule; //
offspringIds: string[]; // id
moduleTree: ModuleTreeNode[];
}>(); }>();
const emit = defineEmits(['init', 'refresh']);
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const appStore = useAppStore();
const userStore = useUserStore();
const { t } = useI18n(); const { t } = useI18n();
const { openModal } = useModal(); const { openModal } = useModal();
const keyword = ref(''); const keyword = ref('');
const showType = ref<'list' | 'mind'>('list'); const showType = ref<'list' | 'mind'>('list');
const filterRowCount = ref(0);
type ResultMap = 0 | 1 | 2 | 3 | 4; const filterConfigList = ref<FilterFormItem[]>([]);
const resultMap = { const tableParams = ref<Record<string, any>>({});
0: {
label: 'caseManagement.caseReview.unReview',
color: 'var(--color-text-input-border)',
icon: 'icon-icon_block_filled',
},
1: {
label: 'caseManagement.caseReview.reviewing',
color: 'rgb(var(--link-6))',
icon: 'icon-icon_testing',
},
2: {
label: 'caseManagement.caseReview.reviewPass',
color: 'rgb(var(--success-6))',
icon: 'icon-icon_succeed_filled',
},
3: {
label: 'caseManagement.caseReview.fail',
color: 'rgb(var(--danger-6))',
icon: 'icon-icon_close_filled',
},
4: {
label: 'caseManagement.caseReview.reReview',
color: 'rgb(var(--warning-6))',
icon: 'icon-icon_resubmit_filled',
},
} as const;
const columns: MsTableColumn = [ const columns: MsTableColumn = [
{ {
title: 'ID', title: 'ID',
dataIndex: 'id', dataIndex: 'num',
sortIndex: 1, sortIndex: 1,
showTooltip: true, showTooltip: true,
width: 100, width: 100,
@ -298,8 +313,8 @@
}, },
{ {
title: 'caseManagement.caseReview.reviewer', title: 'caseManagement.caseReview.reviewer',
dataIndex: 'reviewer', dataIndex: 'reviewNames',
showTooltip: true, slotName: 'reviewNames',
sortable: { sortable: {
sortDirections: ['ascend', 'descend'], sortDirections: ['ascend', 'descend'],
}, },
@ -307,14 +322,14 @@
}, },
{ {
title: 'caseManagement.caseReview.reviewResult', title: 'caseManagement.caseReview.reviewResult',
dataIndex: 'result', dataIndex: 'status',
slotName: 'result', slotName: 'status',
titleSlotName: 'resultColumn', titleSlotName: 'resultColumn',
width: 110, width: 110,
}, },
{ {
title: 'caseManagement.caseReview.version', title: 'caseManagement.caseReview.version',
dataIndex: 'version', dataIndex: 'versionName',
width: 90, width: 90,
}, },
{ {
@ -332,14 +347,17 @@
]; ];
const tableStore = useTableStore(); const tableStore = useTableStore();
tableStore.initColumn(TableKeyEnum.CASE_MANAGEMENT_REVIEW_CASE, columns, 'drawer'); tableStore.initColumn(TableKeyEnum.CASE_MANAGEMENT_REVIEW_CASE, columns, 'drawer');
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(getReviewList, { const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, getTableQueryParams } = useTable(
scroll: { x: '100%' }, getReviewDetailCasePage,
tableKey: TableKeyEnum.CASE_MANAGEMENT_REVIEW_CASE, {
showSetting: true, scroll: { x: '100%' },
selectable: true, tableKey: TableKeyEnum.CASE_MANAGEMENT_REVIEW_CASE,
showSelectAll: true, showSetting: true,
draggable: { type: 'handle', width: 32 }, selectable: true,
}); showSelectAll: true,
draggable: { type: 'handle', width: 32 },
}
);
const batchActions = { const batchActions = {
baseAction: [ baseAction: [
{ {
@ -361,23 +379,54 @@
], ],
}; };
function searchReview() { function searchCase(filter?: FilterResult) {
setLoadListParams({ tableParams.value = {
projectId: appStore.currentProjectId,
reviewId: route.query.id,
moduleIds: props.activeFolder === 'all' ? [] : [props.activeFolder, ...props.offspringIds],
keyword: keyword.value, keyword: keyword.value,
}); viewFlag: props.onlyMine,
combine: filter
? {
...filter.combine,
}
: {},
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
total: propsRes.value.msPagination?.total,
};
setLoadListParams(tableParams.value);
loadList(); loadList();
emit('init', {
...tableParams.value,
moduleIds: [],
});
} }
onBeforeMount(() => { onBeforeMount(() => {
loadList(); searchCase();
}); });
watch(
() => props.onlyMine,
() => {
searchCase();
}
);
watch(
() => props.activeFolder,
() => {
searchCase();
}
);
const tableSelected = ref<(string | number)[]>([]); const tableSelected = ref<(string | number)[]>([]);
const batchParams = ref<BatchActionQueryParams>({ const batchParams = ref<BatchApiParams>({
selectedIds: [], selectIds: [],
selectAll: false, selectAll: false,
excludeIds: [], excludeIds: [] as string[],
currentSelectCount: 0, condition: {},
}); });
/** /**
@ -388,20 +437,16 @@
} }
const dialogVisible = ref<boolean>(false); const dialogVisible = ref<boolean>(false);
const activeRecord = ref({
id: '',
name: '',
status: 0,
});
const defaultDialogForm = { const defaultDialogForm = {
result: 'pass', result: 'PASS',
reason: '', reason: '',
reviewer: [], reviewer: [] as string[],
isAppend: false, isAppend: false,
}; };
const dialogForm = ref({ ...defaultDialogForm }); const dialogForm = ref({ ...defaultDialogForm });
const dialogFormRef = ref<FormInstance>(); const dialogFormRef = ref<FormInstance>();
const dialogShowType = ref<'review' | 'changeReviewer' | 'reReview'>('review'); const dialogShowType = ref<'review' | 'changeReviewer' | 'reReview'>('review'); // review: changeReviewer: reReview:
const dialogLoading = ref(false);
const dialogTitle = computed(() => { const dialogTitle = computed(() => {
switch (dialogShowType.value) { switch (dialogShowType.value) {
case 'review': case 'review':
@ -414,20 +459,6 @@
return ''; return '';
} }
}); });
const reviewersOptions = ref([
{
label: '张三',
value: '1',
},
{
label: '李四',
value: '2',
},
{
label: '王五',
value: '3',
},
]);
function handleDialogCancel() { function handleDialogCancel() {
dialogVisible.value = false; dialogVisible.value = false;
@ -435,8 +466,30 @@
dialogForm.value = { ...defaultDialogForm }; dialogForm.value = { ...defaultDialogForm };
} }
const disassociateLoading = ref(false);
/** /**
* 拦截切换最新版确认 * 解除关联
* @param record 关联用例项
* @param done 关闭弹窗
*/
async function handleDisassociateReviewCase(record: ReviewCaseItem, done) {
try {
disassociateLoading.value = true;
await disassociateReviewCase(route.query.id as string, record.caseId);
emit('refresh');
done();
Message.success(t('caseManagement.caseReview.disassociateSuccess'));
loadList();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
disassociateLoading.value = false;
}
}
/**
* 删除拦截
* @param done 关闭弹窗 * @param done 关闭弹窗
*/ */
async function handleDeleteConfirm(done: (closed: boolean) => void) { async function handleDeleteConfirm(done: (closed: boolean) => void) {
@ -456,7 +509,7 @@
} }
} }
function handleArchive(record: any) { function handleArchive(record: ReviewItem) {
openModal({ openModal({
type: 'warning', type: 'warning',
title: t('caseManagement.caseReview.archivedTitle', { name: record.name }), title: t('caseManagement.caseReview.archivedTitle', { name: record.name }),
@ -482,16 +535,8 @@
}); });
} }
const selectedModuleKeys = ref<(string | number)[]>([]); //
function batchDisassociate() {
/**
* 处理文件夹树节点选中事件
*/
function folderNodeSelect(keys: (string | number)[]) {
selectedModuleKeys.value = keys;
}
function disassociate() {
openModal({ openModal({
type: 'warning', type: 'warning',
title: t('caseManagement.caseReview.disassociateConfirmTitle', { count: tableSelected.value.length }), title: t('caseManagement.caseReview.disassociateConfirmTitle', { count: tableSelected.value.length }),
@ -500,82 +545,157 @@
cancelText: t('common.cancel'), cancelText: t('common.cancel'),
onBeforeOk: async () => { onBeforeOk: async () => {
try { try {
// await resetUserPassword({ dialogLoading.value = true;
// selectIds, await batchDisassociateReviewCase({
// selectAll: !!params?.selectAll, reviewId: route.query.id as string,
// excludeIds: params?.excludeIds || [], userId: props.onlyMine ? userStore.id || '' : '',
// condition: { keyword: keyword.value }, selectIds: batchParams.value.selectIds,
// }); selectAll: batchParams.value.selectAll,
excludeIds: batchParams.value.excludeIds,
condition: batchParams.value.condition,
});
Message.success(t('common.updateSuccess')); Message.success(t('common.updateSuccess'));
resetSelector(); resetSelector();
loadList();
emit('refresh');
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);
} finally {
dialogLoading.value = false;
} }
}, },
hideCancel: false, hideCancel: false,
}); });
} }
function reReview() { //
async function reReview() {
try { try {
dialogLoading.value = true;
await batchReview({
reviewId: route.query.id as string,
userId: props.onlyMine ? userStore.id || '' : '',
reviewPassRule: props.reviewPassRule,
status: 'RE_REVIEWED',
content: dialogForm.value.reason,
notifier: '', // TODO:
selectIds: batchParams.value.selectIds,
selectAll: batchParams.value.selectAll,
excludeIds: batchParams.value.excludeIds,
condition: batchParams.value.condition,
});
Message.success(t('common.updateSuccess')); Message.success(t('common.updateSuccess'));
dialogVisible.value = false; dialogVisible.value = false;
resetSelector(); resetSelector();
emit('refresh');
loadList();
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);
} finally {
dialogLoading.value = false;
} }
} }
//
function changeReviewer() { function changeReviewer() {
dialogFormRef.value?.validate(async (errors) => { dialogFormRef.value?.validate(async (errors) => {
if (!errors) { if (!errors) {
try { try {
dialogLoading.value = true;
await batchChangeReviewer({
reviewId: route.query.id as string,
userId: props.onlyMine ? userStore.id || '' : '',
reviewerId: dialogForm.value.reviewer,
append: dialogForm.value.isAppend, //
selectIds: batchParams.value.selectIds,
selectAll: batchParams.value.selectAll,
excludeIds: batchParams.value.excludeIds,
condition: batchParams.value.condition,
});
Message.success(t('common.updateSuccess')); Message.success(t('common.updateSuccess'));
dialogVisible.value = false; dialogVisible.value = false;
resetSelector(); resetSelector();
loadList();
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);
} finally {
dialogLoading.value = false;
} }
} }
}); });
} }
//
function commitResult() { function commitResult() {
dialogFormRef.value?.validate(async (errors) => { dialogFormRef.value?.validate(async (errors) => {
if (!errors) { if (!errors) {
try { try {
dialogLoading.value = true;
await batchReview({
reviewId: route.query.id as string,
userId: props.onlyMine ? userStore.id || '' : '',
reviewPassRule: props.reviewPassRule,
status: dialogForm.value.result as ReviewResult,
content: dialogForm.value.reason,
notifier: '', // TODO:
selectIds: batchParams.value.selectIds,
selectAll: batchParams.value.selectAll,
excludeIds: batchParams.value.excludeIds,
condition: batchParams.value.condition,
});
Message.success(t('common.updateSuccess')); Message.success(t('common.updateSuccess'));
dialogVisible.value = false; dialogVisible.value = false;
resetSelector(); resetSelector();
emit('refresh');
loadList();
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);
} finally {
dialogLoading.value = false;
} }
} }
}); });
} }
const reviewersOptions = ref<SelectOptionData[]>([]);
const reviewerLoading = ref(false);
async function initReviewers() {
try {
reviewerLoading.value = true;
const res = await getReviewUsers(appStore.currentProjectId, '');
reviewersOptions.value = res.map((e) => ({ label: e.name, value: e.id }));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
reviewerLoading.value = false;
}
}
/** /**
* 处理表格选中后批量操作 * 处理表格选中后批量操作
* @param event 批量操作事件对象 * @param event 批量操作事件对象
*/ */
function handleTableBatch(event: BatchActionParams, params: BatchActionQueryParams) { function handleTableBatch(event: BatchActionParams, params: BatchActionQueryParams) {
tableSelected.value = params?.selectedIds || []; tableSelected.value = params?.selectedIds || [];
batchParams.value = params; batchParams.value = { ...params, selectIds: params?.selectedIds || [], condition: {} };
switch (event.eventTag) { switch (event.eventTag) {
case 'review': case 'review':
dialogVisible.value = true; dialogVisible.value = true;
dialogShowType.value = 'review'; dialogShowType.value = 'review';
break; break;
case 'changeReviewer': case 'changeReviewer':
initReviewers();
dialogVisible.value = true; dialogVisible.value = true;
dialogShowType.value = 'changeReviewer'; dialogShowType.value = 'changeReviewer';
break; break;
case 'disassociate': case 'disassociate':
disassociate(); batchDisassociate();
break; break;
case 'reReview': case 'reReview':
dialogVisible.value = true; dialogVisible.value = true;
@ -586,20 +706,20 @@
} }
} }
function openDetail(id: string) { //
function review(record: ReviewCaseItem) {
router.push({ router.push({
name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW_DETAIL_CASE_DETAIL, name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW_DETAIL_CASE_DETAIL,
query: { query: {
...route.query, ...route.query,
caseId: id, caseId: record.caseId,
},
state: {
params: JSON.stringify(getTableQueryParams()),
}, },
}); });
} }
function review(record: any) {
console.log('review');
}
function createCase() { function createCase() {
router.push({ router.push({
name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE_DETAIL, name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE_DETAIL,
@ -609,12 +729,130 @@
}); });
} }
const associateDrawerVisible = ref(false); onBeforeMount(async () => {
const associateDrawerProject = ref(''); await initReviewers();
filterConfigList.value = [
{
title: 'ID',
dataIndex: 'ID',
type: FilterType.INPUT,
},
{
title: 'caseManagement.caseReview.name',
dataIndex: 'name',
type: FilterType.INPUT,
},
{
title: 'caseManagement.caseReview.caseCount',
dataIndex: 'caseCount',
type: FilterType.NUMBER,
},
{
title: 'caseManagement.caseReview.status',
dataIndex: 'status',
type: FilterType.SELECT,
selectProps: {
mode: 'static',
options: [
{
label: t(reviewStatusMap.PREPARED.label),
value: 'PREPARED',
},
{
label: t(reviewStatusMap.UNDERWAY.label),
value: 'UNDERWAY',
},
{
label: t(reviewStatusMap.COMPLETED.label),
value: 'COMPLETED',
},
{
label: t(reviewStatusMap.ARCHIVED.label),
value: 'ARCHIVED',
},
],
},
},
{
title: 'caseManagement.caseReview.passRate',
dataIndex: 'passRate',
type: FilterType.NUMBER,
},
{
title: 'caseManagement.caseReview.type',
dataIndex: 'reviewPassRule',
type: FilterType.SELECT,
selectProps: {
mode: 'static',
options: [
{
label: t('caseManagement.caseReview.single'),
value: 'SINGLE',
},
{
label: t('caseManagement.caseReview.multi'),
value: 'MULTIPLE',
},
],
},
},
{
title: 'caseManagement.caseReview.reviewer',
dataIndex: 'reviewers',
type: FilterType.SELECT,
selectProps: {
mode: 'static',
options: reviewersOptions.value,
},
},
{
title: 'caseManagement.caseReview.creator',
dataIndex: 'createUser',
type: FilterType.SELECT,
selectProps: {
mode: 'static',
options: reviewersOptions.value,
},
},
{
title: 'caseManagement.caseReview.module',
dataIndex: 'module',
type: FilterType.TREE_SELECT,
treeSelectData: props.moduleTree,
treeSelectProps: {
fieldNames: {
title: 'name',
key: 'id',
children: 'children',
},
},
},
{
title: 'caseManagement.caseReview.tag',
dataIndex: 'tags',
type: FilterType.TAGS_INPUT,
},
{
title: 'caseManagement.caseReview.desc',
dataIndex: 'description',
type: FilterType.INPUT,
},
{
title: 'caseManagement.caseReview.startTime',
dataIndex: 'startTime',
type: FilterType.DATE_PICKER,
},
{
title: 'caseManagement.caseReview.endTime',
dataIndex: 'endTime',
type: FilterType.DATE_PICKER,
},
];
});
function writeAssociateCases(ids: string[]) { defineExpose({
console.log('writeAssociateCases', ids); searchCase,
} });
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -10,7 +10,7 @@
<div :class="getFolderClass('all')" @click="setActiveFolder('all')"> <div :class="getFolderClass('all')" @click="setActiveFolder('all')">
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" /> <MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
<div class="folder-name">{{ t('caseManagement.caseReview.allCases') }}</div> <div class="folder-name">{{ t('caseManagement.caseReview.allCases') }}</div>
<div class="folder-count">({{ allFileCount }})</div> <div class="folder-count">({{ allCount }})</div>
</div> </div>
</div> </div>
<a-divider class="my-[8px]" /> <a-divider class="my-[8px]" />
@ -47,12 +47,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onBeforeMount, ref, watch } from 'vue'; import { computed, onBeforeMount, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useVModel } from '@vueuse/core';
import MsIcon from '@/components/pure/ms-icon-font/index.vue'; import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsTree from '@/components/business/ms-tree/index.vue'; import MsTree from '@/components/business/ms-tree/index.vue';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types'; import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import { getModules } from '@/api/modules/project-management/fileManagement'; import { getReviewDetailModuleTree } from '@/api/modules/case-management/caseReview';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { mapTree } from '@/utils'; import { mapTree } from '@/utils';
@ -63,9 +65,11 @@
modulesCount?: Record<string, number>; // modulesCount?: Record<string, number>; //
showType?: string; // showType?: string; //
isExpandAll?: boolean; // isExpandAll?: boolean; //
selectedKeys: string[]; // key
}>(); }>();
const emit = defineEmits(['init', 'folderNodeSelect']); const emit = defineEmits(['init', 'folderNodeSelect']);
const route = useRoute();
const appStore = useAppStore(); const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
@ -76,7 +80,7 @@
}); });
const activeFolder = ref<string>('all'); const activeFolder = ref<string>('all');
const allFileCount = ref(0); const allCount = ref(0);
const isExpandAll = ref(props.isExpandAll); const isExpandAll = ref(props.isExpandAll);
watch( watch(
@ -99,31 +103,22 @@
const folderTree = ref<ModuleTreeNode[]>([]); const folderTree = ref<ModuleTreeNode[]>([]);
const loading = ref(false); const loading = ref(false);
const selectedKeys = ref<string[]>([]); const selectedKeys = useVModel(props, 'selectedKeys', emit);
/** /**
* 初始化模块树 * 初始化模块树
* @param isSetDefaultKey 是否设置第一个节点为选中节点
*/ */
async function initModules(isSetDefaultKey = false) { async function initModules() {
try { try {
loading.value = true; loading.value = true;
const res = await getModules(appStore.currentProjectId); const res = await getReviewDetailModuleTree(appStore.currentProjectId, route.query.id as string);
folderTree.value = res; folderTree.value = mapTree<ModuleTreeNode>(res, (node) => {
if (isSetDefaultKey) { return {
selectedKeys.value = [folderTree.value[0].id]; ...node,
const offspringIds: string[] = []; count: props.modulesCount?.[node.id] || 0,
mapTree(folderTree.value[0].children || [], (e) => { };
offspringIds.push(e.id); });
return e; emit('init', folderTree.value);
});
emit('folderNodeSelect', selectedKeys.value, offspringIds);
}
emit(
'init',
folderTree.value.map((e) => e.name)
);
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);
@ -141,7 +136,7 @@
offspringIds.push(e.id); offspringIds.push(e.id);
return e; return e;
}); });
activeFolder.value = node.id;
emit('folderNodeSelect', _selectedKeys, offspringIds); emit('folderNodeSelect', _selectedKeys, offspringIds);
} }
@ -161,6 +156,7 @@
count: obj?.[node.id] || 0, count: obj?.[node.id] || 0,
}; };
}); });
allCount.value = obj?.all || 0;
} }
); );

View File

@ -0,0 +1,104 @@
<template>
<a-modal v-model:visible="dialogVisible" class="p-[4px]" title-align="start" body-class="p-0" :mask-closable="false">
<template #title>
<div class="flex items-center justify-start">
<icon-exclamation-circle-fill size="20" class="mr-[8px] text-[rgb(var(--danger-6))]" />
<div class="text-[var(--color-text-1)]">
{{ t('caseManagement.caseReview.deleteReviewTitle', { name: props.record.name }) }}
</div>
</div>
</template>
<div v-if="props.record.status === 'COMPLETED'" class="mb-[10px]">
<div>{{ t('caseManagement.caseReview.deleteFinishedReviewContent1') }}</div>
<div>{{ t('caseManagement.caseReview.deleteFinishedReviewContent2') }}</div>
</div>
<div v-else class="mb-[10px]">
{{
props.record.status === 'UNDERWAY'
? t('caseManagement.caseReview.deleteReviewingContent')
: t('caseManagement.caseReview.deleteReviewContent', {
status: t(reviewStatusMap[props.record.status as ReviewStatus].label),
})
}}
</div>
<a-input
v-model:model-value="confirmReviewName"
:placeholder="t('caseManagement.caseReview.deleteReviewPlaceholder')"
/>
<template #footer>
<div class="flex items-center justify-end">
<a-button type="secondary" @click="handleDialogCancel">{{ t('common.cancel') }}</a-button>
<a-button
type="primary"
status="danger"
:disabled="confirmReviewName !== props.record.name"
class="ml-[12px]"
@click="handleDeleteConfirm"
>
{{ t('common.confirmDelete') }}
</a-button>
<a-button
v-if="props.record.status === 'COMPLETED'"
type="primary"
class="ml-[12px]"
@click="handleDeleteConfirm"
>
{{ t('caseManagement.caseReview.archive') }}
</a-button>
</div>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import { deleteReview } from '@/api/modules/case-management/caseReview';
import { reviewStatusMap } from '@/config/apiTest';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { ReviewStatus } from '@/models/caseManagement/caseReview';
const props = defineProps<{
visible: boolean;
record: {
id: string;
name: string;
status: ReviewStatus;
};
}>();
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
(e: 'success'): void;
}>();
const appStore = useAppStore();
const { t } = useI18n();
const dialogVisible = useVModel(props, 'visible', emit);
const confirmReviewName = ref('');
function handleDialogCancel() {
dialogVisible.value = false;
}
/**
* 删除确认
* @param done 关闭弹窗
*/
async function handleDeleteConfirm() {
try {
await deleteReview(props.record.id, appStore.currentProjectId);
Message.success(t('common.deleteSuccess'));
dialogVisible.value = false;
emit('success');
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
</script>
<style lang="less" scoped></style>

View File

@ -199,6 +199,7 @@
hideMoreAction: e.id === 'root', hideMoreAction: e.id === 'root',
draggable: e.id !== 'root' && !props.isModal, draggable: e.id !== 'root' && !props.isModal,
disabled: e.id === activeFolder.value && props.isModal, disabled: e.id === activeFolder.value && props.isModal,
count: props.modulesCount?.[e.id] || 0, //
}; };
}); });
if (isSetDefaultKey) { if (isSetDefaultKey) {
@ -265,8 +266,8 @@
offspringIds.push(e.id); offspringIds.push(e.id);
return e; return e;
}); });
setActiveFolder(node.id); activeFolder.value = node.id;
emit('folderNodeSelect', _selectedKeys, offspringIds); emit('folderNodeSelect', [node.id], offspringIds);
} }
/** /**
@ -342,6 +343,7 @@
count: obj?.[node.id] || 0, count: obj?.[node.id] || 0,
}; };
}); });
allFileCount.value = obj?.all || 0;
} }
); );

View File

@ -86,7 +86,7 @@
field: props.fieldConfig?.field || '', field: props.fieldConfig?.field || '',
}); });
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>();
const loading = ref(false); const loading = ref(true);
watch( watch(
() => props.fieldConfig?.field, () => props.fieldConfig?.field,

View File

@ -6,8 +6,9 @@
:filter-config-list="filterConfigList" :filter-config-list="filterConfigList"
:row-count="filterRowCount" :row-count="filterRowCount"
:search-placeholder="t('caseManagement.caseReview.searchPlaceholder')" :search-placeholder="t('caseManagement.caseReview.searchPlaceholder')"
@keyword-search="searchReview" @keyword-search="() => searchReview()"
@adv-search="searchReview" @adv-search="searchReview"
@reset="searchReview"
> >
<template #left> <template #left>
<div class="flex items-center"> <div class="flex items-center">
@ -29,23 +30,23 @@
<!-- <template #status-filter> <!-- <template #status-filter>
<a-checkbox-group> <a-checkbox-group>
<a-checkbox :value="0"> <a-checkbox :value="0">
<a-tag :color="statusMap[0].color" :class="statusMap[0].class"> <a-tag :color="reviewStatusMap[0].color" :class="reviewStatusMap[0].class">
{{ t(statusMap[0].label) }} {{ t(reviewStatusMap[0].label) }}
</a-tag> </a-tag>
</a-checkbox> </a-checkbox>
<a-checkbox :value="1"> <a-checkbox :value="1">
<a-tag :color="statusMap[1].color" :class="statusMap[1].class"> <a-tag :color="reviewStatusMap[1].color" :class="reviewStatusMap[1].class">
{{ t(statusMap[1].label) }} {{ t(reviewStatusMap[1].label) }}
</a-tag> </a-tag>
</a-checkbox> </a-checkbox>
<a-checkbox :value="2"> <a-checkbox :value="2">
<a-tag :color="statusMap[2].color" :class="statusMap[2].class"> <a-tag :color="reviewStatusMap[2].color" :class="reviewStatusMap[2].class">
{{ t(statusMap[2].label) }} {{ t(reviewStatusMap[2].label) }}
</a-tag> </a-tag>
</a-checkbox> </a-checkbox>
<a-checkbox :value="3"> <a-checkbox :value="3">
<a-tag :color="statusMap[3].color" :class="statusMap[3].class"> <a-tag :color="reviewStatusMap[3].color" :class="reviewStatusMap[3].class">
{{ t(statusMap[3].label) }} {{ t(reviewStatusMap[3].label) }}
</a-tag> </a-tag>
</a-checkbox> </a-checkbox>
</a-checkbox-group> </a-checkbox-group>
@ -71,16 +72,28 @@
<template #status="{ record }"> <template #status="{ record }">
<statusTag :status="record.status" /> <statusTag :status="record.status" />
</template> </template>
<template #reviewPassRule="{ record }">
{{
record.reviewPassRule === 'SINGLE'
? t('caseManagement.caseReview.single')
: t('caseManagement.caseReview.multi')
}}
</template>
<template #reviewers="{ record }">
<a-tooltip :content="record.reviewers.join('、')">
<div class="one-line-text">{{ record.reviewers.join('、') }}</div>
</a-tooltip>
</template>
<template #passRate="{ record }"> <template #passRate="{ record }">
<div class="mr-[8px] w-[100px]"> <div class="mr-[8px] w-[100px]">
<passRateLine :review-detail="record" height="5px" /> <passRateLine :review-detail="record" height="5px" />
</div> </div>
<div class="text-[var(--color-text-1)]"> <div class="text-[var(--color-text-1)]">
{{ `${(((record.passCount + record.failCount) / record.caseCount) * 100).toFixed(2)}%` }} {{ `${record.passRate}%` }}
</div> </div>
</template> </template>
<template #action="{ record }"> <template #action="{ record }">
<MsButton type="text" class="!mr-0"> <MsButton type="text" class="!mr-0" @click="() => editReview(record)">
{{ t('common.edit') }} {{ t('common.edit') }}
</MsButton> </MsButton>
<a-divider direction="vertical" :margin="8"></a-divider> <a-divider direction="vertical" :margin="8"></a-divider>
@ -99,57 +112,7 @@
</div> </div>
</template> </template>
</ms-base-table> </ms-base-table>
<a-modal <deleteReviewModal v-model:visible="dialogVisible" :record="activeRecord" @success="loadList" />
v-model:visible="dialogVisible"
:on-before-ok="handleDeleteConfirm"
class="p-[4px]"
title-align="start"
body-class="p-0"
:mask-closable="false"
>
<template #title>
<div class="flex items-center justify-start">
<icon-exclamation-circle-fill size="20" class="mr-[8px] text-[rgb(var(--danger-6))]" />
<div class="text-[var(--color-text-1)]">
{{ t('caseManagement.caseReview.deleteReviewTitle', { name: activeRecord.name }) }}
</div>
</div>
</template>
<div v-if="activeRecord.status === 2" class="mb-[10px]">
<div>{{ t('caseManagement.caseReview.deleteFinishedReviewContent1') }}</div>
<div>{{ t('caseManagement.caseReview.deleteFinishedReviewContent2') }}</div>
</div>
<div v-else class="mb-[10px]">
{{
activeRecord.status === 1
? t('caseManagement.caseReview.deleteReviewingContent')
: t('caseManagement.caseReview.deleteReviewContent', {
status: t(statusMap[activeRecord.status as StatusMap].label),
})
}}
</div>
<a-input
v-model:model-value="confirmReviewName"
:placeholder="t('caseManagement.caseReview.deleteReviewPlaceholder')"
/>
<template #footer>
<div class="flex items-center justify-end">
<a-button type="secondary" @click="handleDialogCancel">{{ t('common.cancel') }}</a-button>
<a-button
type="primary"
status="danger"
:disabled="confirmReviewName !== activeRecord.name"
class="ml-[12px]"
@click="handleDialogCancel"
>
{{ t('common.confirmDelete') }}
</a-button>
<a-button v-if="activeRecord.status === 2" type="primary" class="ml-[12px]" @click="handleDialogCancel">
{{ t('caseManagement.caseReview.archive') }}
</a-button>
</div>
</template>
</a-modal>
<a-modal <a-modal
v-model:visible="moveModalVisible" v-model:visible="moveModalVisible"
title-align="start" title-align="start"
@ -188,7 +151,7 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { MsAdvanceFilter } from '@/components/pure/ms-advance-filter'; import { MsAdvanceFilter } from '@/components/pure/ms-advance-filter';
import { FilterFormItem, FilterType } from '@/components/pure/ms-advance-filter/type'; import { FilterFormItem, FilterResult, FilterType } from '@/components/pure/ms-advance-filter/type';
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue'; import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type'; import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type';
@ -197,27 +160,40 @@
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types'; import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import passRateLine from '../passRateLine.vue'; import passRateLine from '../passRateLine.vue';
import statusTag from '../statusTag.vue'; import statusTag from '../statusTag.vue';
import deleteReviewModal from './deleteReviewModal.vue';
import ModuleTree from './moduleTree.vue'; import ModuleTree from './moduleTree.vue';
import { getReviewList, getReviewUsers } from '@/api/modules/case-management/caseReview'; import { getReviewList, getReviewUsers } from '@/api/modules/case-management/caseReview';
import { reviewStatusMap } from '@/config/apiTest';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal'; import useModal from '@/hooks/useModal';
import useTableStore from '@/hooks/useTableStore'; import useTableStore from '@/hooks/useTableStore';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import useUserStore from '@/store/modules/user';
import {
ReviewDetailReviewersItem,
ReviewItem,
ReviewListQueryParams,
ReviewStatus,
} from '@/models/caseManagement/caseReview';
import type { ModuleTreeNode } from '@/models/projectManagement/file'; import type { ModuleTreeNode } from '@/models/projectManagement/file';
import { CaseManagementRouteEnum } from '@/enums/routeEnum'; import { CaseManagementRouteEnum } from '@/enums/routeEnum';
import { TableKeyEnum } from '@/enums/tableEnum'; import { TableKeyEnum } from '@/enums/tableEnum';
const props = defineProps<{ const props = defineProps<{
activeFolder: string | number; activeFolder: string;
moduleTree: ModuleTreeNode[]; moduleTree: ModuleTreeNode[];
showType: string;
offspringIds: string[];
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'goCreate'): void; (e: 'goCreate'): void;
(e: 'init', params: ReviewListQueryParams): void;
}>(); }>();
const userStore = useUserStore();
const appStore = useAppStore(); const appStore = useAppStore();
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
@ -225,35 +201,6 @@
const keyword = ref(''); const keyword = ref('');
type StatusMap = 0 | 1 | 2 | 3;
const statusMap = {
0: {
label: 'caseManagement.caseReview.unStart',
color: 'var(--color-text-n8)',
class: '!text-[var(--color-text-1)]',
},
1: {
label: 'caseManagement.caseReview.going',
color: 'rgb(var(--link-2))',
class: '!text-[rgb(var(--link-6))]',
},
2: {
label: 'caseManagement.caseReview.finished',
color: 'rgb(var(--success-2))',
class: '!text-[rgb(var(--success-6))]',
},
3: {
label: 'caseManagement.caseReview.archived',
color: 'var(--color-text-n8)',
class: '!text-[var(--color-text-4)]',
},
} as const;
const typeMap = {
single: 'caseManagement.caseReview.single',
multi: 'caseManagement.caseReview.multi',
};
const filterRowCount = ref(0); const filterRowCount = ref(0);
const filterConfigList = ref<FilterFormItem[]>([]); const filterConfigList = ref<FilterFormItem[]>([]);
@ -285,19 +232,19 @@
mode: 'static', mode: 'static',
options: [ options: [
{ {
label: t(statusMap[0].label), label: t(reviewStatusMap.PREPARED.label),
value: 'PREPARED', value: 'PREPARED',
}, },
{ {
label: t(statusMap[1].label), label: t(reviewStatusMap.UNDERWAY.label),
value: 'UNDERWAY', value: 'UNDERWAY',
}, },
{ {
label: t(statusMap[2].label), label: t(reviewStatusMap.COMPLETED.label),
value: 'COMPLETED', value: 'COMPLETED',
}, },
{ {
label: t(statusMap[3].label), label: t(reviewStatusMap.ARCHIVED.label),
value: 'ARCHIVED', value: 'ARCHIVED',
}, },
], ],
@ -310,7 +257,7 @@
}, },
{ {
title: 'caseManagement.caseReview.type', title: 'caseManagement.caseReview.type',
dataIndex: 'type', dataIndex: 'reviewPassRule',
type: FilterType.SELECT, type: FilterType.SELECT,
selectProps: { selectProps: {
mode: 'static', mode: 'static',
@ -328,7 +275,7 @@
}, },
{ {
title: 'caseManagement.caseReview.reviewer', title: 'caseManagement.caseReview.reviewer',
dataIndex: 'reviewer', dataIndex: 'reviewers',
type: FilterType.SELECT, type: FilterType.SELECT,
selectProps: { selectProps: {
mode: 'static', mode: 'static',
@ -337,7 +284,7 @@
}, },
{ {
title: 'caseManagement.caseReview.creator', title: 'caseManagement.caseReview.creator',
dataIndex: 'creator', dataIndex: 'createUser',
type: FilterType.SELECT, type: FilterType.SELECT,
selectProps: { selectProps: {
mode: 'static', mode: 'static',
@ -364,12 +311,17 @@
}, },
{ {
title: 'caseManagement.caseReview.desc', title: 'caseManagement.caseReview.desc',
dataIndex: 'desc', dataIndex: 'description',
type: FilterType.INPUT, type: FilterType.INPUT,
}, },
{ {
title: 'caseManagement.caseReview.cycle', title: 'caseManagement.caseReview.startTime',
dataIndex: 'cycle', dataIndex: 'startTime',
type: FilterType.DATE_PICKER,
},
{
title: 'caseManagement.caseReview.endTime',
dataIndex: 'endTime',
type: FilterType.DATE_PICKER, type: FilterType.DATE_PICKER,
}, },
]; ];
@ -382,10 +334,10 @@
const columns: MsTableColumn = [ const columns: MsTableColumn = [
{ {
title: 'ID', title: 'ID',
dataIndex: 'id', dataIndex: 'num',
sortIndex: 1, sortIndex: 1,
showTooltip: true, showTooltip: true,
width: 90, width: 100,
}, },
{ {
title: 'caseManagement.caseReview.name', title: 'caseManagement.caseReview.name',
@ -415,13 +367,14 @@
}, },
{ {
title: 'caseManagement.caseReview.type', title: 'caseManagement.caseReview.type',
dataIndex: 'type', slotName: 'reviewPassRule',
dataIndex: 'reviewPassRule',
width: 90, width: 90,
}, },
{ {
title: 'caseManagement.caseReview.reviewer', title: 'caseManagement.caseReview.reviewer',
dataIndex: 'reviewer', slotName: 'reviewers',
showTooltip: true, dataIndex: 'reviewers',
sortable: { sortable: {
sortDirections: ['ascend', 'descend'], sortDirections: ['ascend', 'descend'],
}, },
@ -429,7 +382,7 @@
}, },
{ {
title: 'caseManagement.caseReview.creator', title: 'caseManagement.caseReview.creator',
dataIndex: 'creator', dataIndex: 'createUser',
width: 90, width: 90,
}, },
{ {
@ -445,14 +398,14 @@
}, },
{ {
title: 'caseManagement.caseReview.desc', title: 'caseManagement.caseReview.desc',
dataIndex: 'desc', dataIndex: 'description',
width: 150, width: 150,
showTooltip: true, showTooltip: true,
}, },
{ {
title: 'caseManagement.caseReview.cycle', title: 'caseManagement.caseReview.cycle',
dataIndex: 'cycle', dataIndex: 'cycle',
width: 340, width: 350,
}, },
{ {
title: 'common.operation', title: 'common.operation',
@ -475,9 +428,9 @@
(item) => { (item) => {
return { return {
...item, ...item,
type: t(typeMap[item.type as keyof typeof typeMap]), tags: (item.tags || []).map((e: string) => ({ id: e, name: e })),
tags: item.tags?.map((e: string) => ({ id: e, name: e })) || [], reviewers: item.reviewers.map((e: ReviewDetailReviewersItem) => e.userName),
cycle: `${dayjs(item.cycle[0]).format('YYYY-MM-DD HH:mm:ss')} - ${dayjs(item.cycle[1]).format( cycle: `${dayjs(item.startTime).format('YYYY-MM-DD HH:mm:ss')} - ${dayjs(item.endTime).format(
'YYYY-MM-DD HH:mm:ss' 'YYYY-MM-DD HH:mm:ss'
)}`, )}`,
}; };
@ -492,19 +445,43 @@
], ],
}; };
function searchReview() { const tableQueryParams = ref<any>();
setLoadListParams({ function searchReview(filter?: FilterResult) {
const params = {
keyword: keyword.value, keyword: keyword.value,
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
moduleIds: props.activeFolder === 'all' ? [] : [props.activeFolder], moduleIds: props.activeFolder === 'all' ? [] : [props.activeFolder, ...props.offspringIds],
}); createByMe: props.showType === 'createByMe' ? userStore.id : undefined,
reviewByMe: props.showType === 'reviewByMe' ? userStore.id : undefined,
combine: filter
? {
...filter.combine,
}
: {},
};
setLoadListParams(params);
loadList(); loadList();
tableQueryParams.value = {
...params,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
};
emit('init', {
...tableQueryParams.value,
});
} }
onBeforeMount(() => { onBeforeMount(() => {
searchReview(); searchReview();
}); });
watch(
() => props.showType,
() => {
searchReview();
}
);
const tableSelected = ref<(string | number)[]>([]); const tableSelected = ref<(string | number)[]>([]);
const batchParams = ref<BatchActionQueryParams>({ const batchParams = ref<BatchActionQueryParams>({
selectedIds: [], selectedIds: [],
@ -524,35 +501,9 @@
const activeRecord = ref({ const activeRecord = ref({
id: '', id: '',
name: '', name: '',
status: 0, status: 'PREPARED' as ReviewStatus,
}); });
const confirmReviewName = ref(''); const confirmReviewName = ref('');
function handleDialogCancel() {
dialogVisible.value = false;
}
/**
* 删除确认
* @param done 关闭弹窗
*/
async function handleDeleteConfirm(done: (closed: boolean) => void) {
try {
// if (replaceVersion.value !== '') {
// await useLatestVersion(replaceVersion.value);
// }
// await toggleVersionStatus(activeRecord.value.id);
// Message.success(t('caseManagement.caseReview.close', { name: activeRecord.value.name }));
loadList();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
done(false);
} finally {
done(true);
}
}
/** /**
* 根据评审状态获取更多按钮列表 * 根据评审状态获取更多按钮列表
* @param status 评审状态 * @param status 评审状态
@ -583,7 +534,16 @@
]; ];
} }
function handleArchive(record: any) { function editReview(record: ReviewItem) {
router.push({
name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW_CREATE,
query: {
id: record.id,
},
});
}
function handleArchive(record: ReviewItem) {
openModal({ openModal({
type: 'warning', type: 'warning',
title: t('caseManagement.caseReview.archivedTitle', { name: record.name }), title: t('caseManagement.caseReview.archivedTitle', { name: record.name }),
@ -670,7 +630,7 @@
* 处理表格更多按钮事件 * 处理表格更多按钮事件
* @param item * @param item
*/ */
function handleMoreActionSelect(item: ActionsItem, record: any) { function handleMoreActionSelect(item: ActionsItem, record: ReviewItem) {
switch (item.eventTag) { switch (item.eventTag) {
case 'delete': case 'delete':
activeRecord.value = record; activeRecord.value = record;

View File

@ -1,26 +1,5 @@
<template> <template>
<MsColorLine <MsColorLine :color-data="colorData" :height="props.height" :radius="props.radius">
:color-data="[
{
percentage: (props.reviewDetail.passCount / props.reviewDetail.caseCount) * 100,
color: 'rgb(var(--success-6))',
},
{
percentage: (props.reviewDetail.failCount / props.reviewDetail.caseCount) * 100,
color: 'rgb(var(--danger-6))',
},
{
percentage: (props.reviewDetail.reviewCount / props.reviewDetail.caseCount) * 100,
color: 'rgb(var(--warning-6))',
},
{
percentage: (props.reviewDetail.reviewingCount / props.reviewDetail.caseCount) * 100,
color: 'rgb(var(--link-6))',
},
]"
:height="props.height"
:radius="props.radius"
>
<template #popoverContent> <template #popoverContent>
<table> <table>
<tr> <tr>
@ -28,15 +7,13 @@
<td class="font-medium text-[var(--color-text-1)]"> <td class="font-medium text-[var(--color-text-1)]">
{{ {{
`${( `${(
((props.reviewDetail.passCount + props.reviewDetail.failCount) / props.reviewDetail.caseCount) * ((props.reviewDetail.passCount + props.reviewDetail.unPassCount) / props.reviewDetail.caseCount) *
100 100
).toFixed(2)}%` ).toFixed(2)}%`
}} }}
<span <span>
>({{ ({{ `${props.reviewDetail.passCount + props.reviewDetail.unPassCount}/${props.reviewDetail.caseCount}` }})
`${props.reviewDetail.passCount + props.reviewDetail.failCount}/${props.reviewDetail.caseCount}` </span>
}})</span
>
</td> </td>
</tr> </tr>
<tr> <tr>
@ -54,7 +31,7 @@
<div>{{ t('caseManagement.caseReview.fail') }}</div> <div>{{ t('caseManagement.caseReview.fail') }}</div>
</td> </td>
<td class="popover-value-td"> <td class="popover-value-td">
{{ props.reviewDetail.failCount }} {{ props.reviewDetail.unPassCount }}
</td> </td>
</tr> </tr>
<tr> <tr>
@ -63,7 +40,7 @@
<div>{{ t('caseManagement.caseReview.reReview') }}</div> <div>{{ t('caseManagement.caseReview.reReview') }}</div>
</td> </td>
<td class="popover-value-td"> <td class="popover-value-td">
{{ props.reviewDetail.reviewCount }} {{ props.reviewDetail.reviewedCount }}
</td> </td>
</tr> </tr>
<tr> <tr>
@ -72,7 +49,7 @@
<div>{{ t('caseManagement.caseReview.reviewing') }}</div> <div>{{ t('caseManagement.caseReview.reviewing') }}</div>
</td> </td>
<td class="popover-value-td"> <td class="popover-value-td">
{{ props.reviewDetail.reviewingCount }} {{ props.reviewDetail.underReviewedCount }}
</td> </td>
</tr> </tr>
</table> </table>
@ -88,9 +65,9 @@
const props = defineProps<{ const props = defineProps<{
reviewDetail: { reviewDetail: {
passCount: number; passCount: number;
failCount: number; unPassCount: number;
reviewCount: number; reviewedCount: number;
reviewingCount: number; underReviewedCount: number;
caseCount: number; caseCount: number;
[key: string]: any; [key: string]: any;
}; };
@ -98,6 +75,41 @@
radius?: string; radius?: string;
}>(); }>();
const { t } = useI18n(); const { t } = useI18n();
const colorData = computed(() => {
if (
props.reviewDetail.status === 'PREPARED' ||
(props.reviewDetail.passCount === 0 &&
props.reviewDetail.unPassCount === 0 &&
props.reviewDetail.reviewedCount === 0 &&
props.reviewDetail.underReviewedCount === 0)
) {
return [
{
percentage: 100,
color: 'var(--color-text-n8)',
},
];
}
return [
{
percentage: (props.reviewDetail.passCount / props.reviewDetail.caseCount) * 100,
color: 'rgb(var(--success-6))',
},
{
percentage: (props.reviewDetail.unPassCount / props.reviewDetail.caseCount) * 100,
color: 'rgb(var(--danger-6))',
},
{
percentage: (props.reviewDetail.reviewedCount / props.reviewDetail.caseCount) * 100,
color: 'rgb(var(--warning-6))',
},
{
percentage: (props.reviewDetail.underReviewedCount / props.reviewDetail.caseCount) * 100,
color: 'rgb(var(--link-6))',
},
];
});
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -7,30 +7,30 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
export type StatusMap = 0 | 1 | 2 | 3; import type { ReviewStatus } from '@/models/caseManagement/caseReview';
const props = defineProps<{ const props = defineProps<{
status: StatusMap; status: ReviewStatus;
}>(); }>();
const { t } = useI18n(); const { t } = useI18n();
const statusMap = { const statusMap = {
0: { PREPARED: {
label: 'caseManagement.caseReview.unStart', label: 'caseManagement.caseReview.unStart',
color: 'var(--color-text-n8)', color: 'var(--color-text-n8)',
class: '!text-[var(--color-text-1)]', class: '!text-[var(--color-text-1)]',
}, },
1: { UNDERWAY: {
label: 'caseManagement.caseReview.going', label: 'caseManagement.caseReview.going',
color: 'rgb(var(--link-2))', color: 'rgb(var(--link-2))',
class: '!text-[rgb(var(--link-6))]', class: '!text-[rgb(var(--link-6))]',
}, },
2: { COMPLETED: {
label: 'caseManagement.caseReview.finished', label: 'caseManagement.caseReview.finished',
color: 'rgb(var(--success-2))', color: 'rgb(var(--success-2))',
class: '!text-[rgb(var(--success-6))]', class: '!text-[rgb(var(--success-6))]',
}, },
3: { ARCHIVED: {
label: 'caseManagement.caseReview.archived', label: 'caseManagement.caseReview.archived',
color: 'var(--color-text-n8)', color: 'var(--color-text-n8)',
class: '!text-[var(--color-text-4)]', class: '!text-[var(--color-text-4)]',

View File

@ -1,6 +1,7 @@
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<template> <template>
<MsCard <MsCard
:loading="loading"
:title="isEdit ? t('menu.caseManagement.caseManagementCaseReviewEdit') : t('caseManagement.caseReview.create')" :title="isEdit ? t('menu.caseManagement.caseManagementCaseReviewEdit') : t('caseManagement.caseReview.create')"
> >
<a-form ref="reviewFormRef" class="w-[732px]" :model="reviewForm" layout="vertical"> <a-form ref="reviewFormRef" class="w-[732px]" :model="reviewForm" layout="vertical">
@ -26,18 +27,19 @@
/> />
</a-form-item> </a-form-item>
<a-form-item field="folderId" :label="t('caseManagement.caseReview.belongModule')"> <a-form-item field="folderId" :label="t('caseManagement.caseReview.belongModule')">
<a-select <a-tree-select
v-model:modelValue="reviewForm.folderId" v-model:modelValue="reviewForm.folderId"
:placeholder="t('caseManagement.caseReview.belongModulePlaceholder')" :placeholder="t('caseManagement.caseReview.belongModulePlaceholder')"
:options="moduleOptions" :data="moduleOptions"
class="w-[436px]" class="w-[436px]"
:field-names="{ title: 'name', key: 'id', children: 'children' }"
:loading="moduleLoading"
allow-search allow-search
multiple
/> />
</a-form-item> </a-form-item>
<a-form-item field="type" :label="t('caseManagement.caseReview.type')"> <a-form-item field="type" :label="t('caseManagement.caseReview.type')">
<a-radio-group v-model:modelValue="reviewForm.type"> <a-radio-group v-model:modelValue="reviewForm.type" :disabled="isEdit">
<a-radio value="single"> <a-radio value="SINGLE">
<div class="flex items-center"> <div class="flex items-center">
{{ t('caseManagement.caseReview.single') }} {{ t('caseManagement.caseReview.single') }}
<a-tooltip :content="t('caseManagement.caseReview.singleTip')" position="right"> <a-tooltip :content="t('caseManagement.caseReview.singleTip')" position="right">
@ -48,7 +50,7 @@
</a-tooltip> </a-tooltip>
</div> </div>
</a-radio> </a-radio>
<a-radio value="multi"> <a-radio value="MULTIPLE">
<div class="flex items-center"> <div class="flex items-center">
{{ t('caseManagement.caseReview.multi') }} {{ t('caseManagement.caseReview.multi') }}
<a-tooltip :content="t('caseManagement.caseReview.multiTip')" position="right"> <a-tooltip :content="t('caseManagement.caseReview.multiTip')" position="right">
@ -83,10 +85,19 @@
:placeholder="t('caseManagement.caseReview.reviewerPlaceholder')" :placeholder="t('caseManagement.caseReview.reviewerPlaceholder')"
:options="reviewersOptions" :options="reviewersOptions"
:search-keys="['label']" :search-keys="['label']"
allow-clear
allow-search allow-search
multiple multiple
:loading="reviewerLoading" :loading="reviewerLoading"
/> class="reviewer-select"
>
<template #label="data">
<div class="flex items-center gap-[2px]">
<MsAvatar :avatar="reviewersOptions.find((e) => e.value === data.value)?.avatar" :size="20" />
{{ data.label }}
</div>
</template>
</MsSelect>
</a-form-item> </a-form-item>
<a-form-item field="tags" :label="t('caseManagement.caseReview.tag')"> <a-form-item field="tags" :label="t('caseManagement.caseReview.tag')">
<MsTagsInput v-model:model-value="reviewForm.tags" /> <MsTagsInput v-model:model-value="reviewForm.tags" />
@ -95,6 +106,7 @@
<a-range-picker <a-range-picker
v-model:model-value="reviewForm.cycle" v-model:model-value="reviewForm.cycle"
show-time show-time
value-format="timestamp"
:time-picker-props="{ :time-picker-props="{
defaultValue: ['00:00:00', '00:00:00'], defaultValue: ['00:00:00', '00:00:00'],
}" }"
@ -105,8 +117,13 @@
<template #label> <template #label>
<div class="flex items-center"> <div class="flex items-center">
<div>{{ t('caseManagement.caseReview.pickCases') }}</div> <div>{{ t('caseManagement.caseReview.pickCases') }}</div>
<a-divider margin="4px" direction="vertical" /> <a-divider v-if="!isCopy" margin="4px" direction="vertical" />
<MsButton type="text" :disabled="selectedAssociateCases.length === 0" @click="clearSelectedCases"> <MsButton
v-if="!isCopy"
type="text"
:disabled="selectedAssociateCasesParams.selectIds.length === 0"
@click="clearSelectedCases"
>
{{ t('caseManagement.caseReview.clearSelectedCases') }} {{ t('caseManagement.caseReview.clearSelectedCases') }}
</MsButton> </MsButton>
</div> </div>
@ -114,10 +131,14 @@
<div class="bg-[var(--color-text-n9)] p-[12px]"> <div class="bg-[var(--color-text-n9)] p-[12px]">
<div class="flex items-center"> <div class="flex items-center">
<div class="text-[var(--color-text-2)]"> <div class="text-[var(--color-text-2)]">
{{ t('caseManagement.caseReview.selectedCases', { count: selectedAssociateCases.length }) }} {{
t('caseManagement.caseReview.selectedCases', {
count: isCopy ? reviewForm.caseCount : selectedAssociateCasesParams.selectIds.length,
})
}}
</div> </div>
<a-divider margin="8px" direction="vertical" /> <a-divider v-if="!isCopy" margin="8px" direction="vertical" />
<MsButton type="text" class="font-medium" @click="caseAssociateVisible = true"> <MsButton v-if="!isCopy" type="text" class="font-medium" @click="caseAssociateVisible = true">
{{ t('ms.case.associate.title') }} {{ t('ms.case.associate.title') }}
</MsButton> </MsButton>
</div> </div>
@ -126,13 +147,15 @@
</a-form> </a-form>
<template #footerRight> <template #footerRight>
<div class="flex items-center"> <div class="flex items-center">
<a-button type="secondary" @click="cancelCreate">{{ t('common.cancel') }}</a-button> <a-button type="secondary" :disabled="saveLoading" @click="cancelCreate">{{ t('common.cancel') }}</a-button>
<a-button v-if="isEdit" type="primary" class="ml-[16px]" @click="updateReview"> <a-button v-if="isEdit" type="primary" class="ml-[16px]" :loading="saveLoading" @click="updateReview">
{{ t('common.update') }} {{ t('common.update') }}
</a-button> </a-button>
<template v-else> <template v-else>
<a-button type="secondary" class="mx-[16px]" @click="() => saveReview()">{{ t('common.save') }}</a-button> <a-button type="secondary" class="mx-[16px]" :loading="saveLoading" @click="() => saveReview()">
<a-button type="primary" @click="() => saveReview(true)"> {{ t('common.save') }}
</a-button>
<a-button type="primary" :disabled="saveLoading" @click="() => saveReview(true)">
{{ t('caseManagement.caseReview.review') }} {{ t('caseManagement.caseReview.review') }}
</a-button> </a-button>
</template> </template>
@ -154,16 +177,25 @@
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { Message, SelectOptionData } from '@arco-design/web-vue'; import { Message, SelectOptionData } from '@arco-design/web-vue';
import MsAvatar from '@/components/pure/ms-avatar/index.vue';
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import MsCard from '@/components/pure/ms-card/index.vue'; import MsCard from '@/components/pure/ms-card/index.vue';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue'; import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import MsSelect from '@/components/business/ms-select'; import MsSelect from '@/components/business/ms-select';
import AssociateDrawer from './components/create/associateDrawer.vue'; import AssociateDrawer from './components/create/associateDrawer.vue';
import { getReviewUsers } from '@/api/modules/case-management/caseReview'; import {
addReview,
copyReview,
editReview,
getReviewDetail,
getReviewModules,
getReviewUsers,
} from '@/api/modules/case-management/caseReview';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import type { BaseAssociateCaseRequest, ReviewPassRule } from '@/models/caseManagement/caseReview';
import { CaseManagementRouteEnum } from '@/enums/routeEnum'; import { CaseManagementRouteEnum } from '@/enums/routeEnum';
import type { FormInstance } from '@arco-design/web-vue'; import type { FormInstance } from '@arco-design/web-vue';
@ -174,30 +206,36 @@
const { t } = useI18n(); const { t } = useI18n();
const isEdit = ref(!!route.query.id); const isEdit = ref(!!route.query.id);
const isCopy = ref(!!route.query.copyId);
const reviewFormRef = ref<FormInstance>(); const reviewFormRef = ref<FormInstance>();
const reviewForm = ref({ const reviewForm = ref({
name: '', name: '',
desc: '', desc: '',
folderId: '', folderId: (route.query.moduleId as string) || 'root',
type: 'single', type: 'SINGLE' as ReviewPassRule,
reviewers: [], reviewers: [] as string[],
tags: [], tags: [] as string[],
cycle: [], cycle: [] as number[],
caseCount: 0,
}); });
const moduleOptions = ref([ const moduleOptions = ref<SelectOptionData[]>([]);
{ const moduleLoading = ref(false);
label: '全部',
value: 'all', /**
}, * 初始化模块选择
{ */
label: '模块1', async function initModules() {
value: '1', try {
}, moduleLoading.value = true;
{ moduleOptions.value = await getReviewModules(appStore.currentProjectId);
label: '模块2', } catch (error) {
value: '2', // eslint-disable-next-line no-console
}, console.log(error);
]); } finally {
moduleLoading.value = false;
}
}
const reviewersOptions = ref<SelectOptionData[]>([]); const reviewersOptions = ref<SelectOptionData[]>([]);
const reviewerLoading = ref(false); const reviewerLoading = ref(false);
@ -205,7 +243,7 @@
try { try {
reviewerLoading.value = true; reviewerLoading.value = true;
const res = await getReviewUsers(appStore.currentProjectId, ''); const res = await getReviewUsers(appStore.currentProjectId, '');
reviewersOptions.value = res.map((e) => ({ label: e.name, value: e.id })); reviewersOptions.value = res.map((e) => ({ label: e.name, value: e.id, avatar: e.avatar }));
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);
@ -214,33 +252,94 @@
} }
} }
const selectedAssociateCases = ref<string[]>([]); //
const selectedAssociateCasesParams = ref<BaseAssociateCaseRequest>({
excludeIds: [],
selectIds: [],
selectAll: false,
condition: {},
moduleIds: [],
versionId: '',
refId: '',
projectId: '',
});
function writeAssociateCases(ids: string[]) { function writeAssociateCases(param: BaseAssociateCaseRequest) {
selectedAssociateCases.value = [...ids]; selectedAssociateCasesParams.value = { ...param };
} }
function clearSelectedCases() { function clearSelectedCases() {
selectedAssociateCases.value = []; selectedAssociateCasesParams.value = {
excludeIds: [],
selectIds: [],
selectAll: false,
condition: {},
moduleIds: [],
versionId: '',
refId: '',
projectId: '',
};
} }
function cancelCreate() { function cancelCreate() {
router.back(); router.back();
} }
const saveLoading = ref(false);
function saveReview(isGoReview = false) { function saveReview(isGoReview = false) {
reviewFormRef.value?.validate(async (errors) => { reviewFormRef.value?.validate(async (errors) => {
if (!errors) { if (!errors) {
Message.success(t('common.createSuccess')); try {
if (isGoReview) { saveLoading.value = true;
// const { name, folderId, type, cycle, tags, desc, reviewers } = reviewForm.value;
router.replace({ let res = '';
name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW, if (isCopy.value) {
}); //
} else { res = await copyReview({
router.replace({ copyId: route.query.copyId as string,
name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW, projectId: appStore.currentProjectId,
}); name,
moduleId: folderId,
reviewPassRule: type, //
startTime: cycle[0],
endTime: cycle[1],
tags,
description: desc,
reviewers, //
});
} else {
res = await addReview({
projectId: appStore.currentProjectId,
name,
moduleId: folderId,
reviewPassRule: type, //
startTime: cycle[0],
endTime: cycle[1],
tags,
description: desc,
reviewers, //
baseAssociateCaseRequest: selectedAssociateCasesParams.value, //
});
}
Message.success(t('common.createSuccess'));
if (isGoReview) {
//
router.replace({
name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW_DETAIL,
query: {
id: res,
},
});
} else {
router.replace({
name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW,
});
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
saveLoading.value = false;
} }
} }
}); });
@ -249,19 +348,65 @@
function updateReview() { function updateReview() {
reviewFormRef.value?.validate(async (errors) => { reviewFormRef.value?.validate(async (errors) => {
if (!errors) { if (!errors) {
Message.success(t('common.updateSuccess')); try {
router.replace({ saveLoading.value = true;
name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW, const { name, folderId, type, cycle, tags, desc, reviewers } = reviewForm.value;
}); await editReview({
id: route.query.id as string,
projectId: appStore.currentProjectId,
name,
moduleId: folderId,
reviewPassRule: type, //
startTime: cycle[0],
endTime: cycle[1],
tags,
description: desc,
reviewers, //
});
Message.success(t('common.updateSuccess'));
router.back();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
saveLoading.value = false;
}
} }
}); });
} }
const caseAssociateVisible = ref<boolean>(false); const caseAssociateVisible = ref<boolean>(false);
const caseAssociateProject = ref(''); const caseAssociateProject = ref('');
const loading = ref(false);
async function initReviewDetail() {
try {
loading.value = true;
const res = await getReviewDetail((route.query.copyId as string) || (route.query.id as string) || '');
reviewForm.value = {
name: res.name,
desc: res.description,
folderId: res.moduleId,
type: res.reviewPassRule,
reviewers: res.reviewers.map((e) => e.userId),
tags: res.tags,
cycle: [res.startTime, res.endTime],
caseCount: res.caseCount,
};
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
onBeforeMount(() => { onBeforeMount(() => {
initModules();
initReviewers(); initReviewers();
if (isEdit.value || isCopy.value) {
//
initReviewDetail();
}
}); });
</script> </script>
@ -269,4 +414,11 @@
:deep(.arco-form-item-label-col) { :deep(.arco-form-item-label-col) {
@apply w-auto flex-none; @apply w-auto flex-none;
} }
:deep(.reviewer-select) {
.arco-select-view-tag {
@apply rounded-full;
padding-left: 2px;
}
}
</style> </style>

View File

@ -1,5 +1,5 @@
<template> <template>
<MsCard :min-width="1100" auto-height hide-footer no-bottom-radius no-content-padding hide-divider> <MsCard :loading="loading" :min-width="1100" auto-height hide-footer no-bottom-radius no-content-padding hide-divider>
<template #headerLeft> <template #headerLeft>
<a-tooltip :content="reviewDetail.name"> <a-tooltip :content="reviewDetail.name">
<div class="one-line-text mr-[8px] max-w-[260px] font-medium text-[var(--color-text-000)]"> <div class="one-line-text mr-[8px] max-w-[260px] font-medium text-[var(--color-text-000)]">
@ -10,43 +10,39 @@
class="rounded-[0_999px_999px_0] border border-solid border-[text-[rgb(var(--primary-5))]] px-[8px] py-[2px] text-[12px] leading-[16px] text-[rgb(var(--primary-5))]" class="rounded-[0_999px_999px_0] border border-solid border-[text-[rgb(var(--primary-5))]] px-[8px] py-[2px] text-[12px] leading-[16px] text-[rgb(var(--primary-5))]"
> >
<MsIcon type="icon-icon-contacts" size="13" /> <MsIcon type="icon-icon-contacts" size="13" />
{{ t('caseManagement.caseReview.single') }} {{
reviewDetail.reviewPassRule === 'SINGLE'
? t('caseManagement.caseReview.single')
: t('caseManagement.caseReview.multi')
}}
</div> </div>
<statusTag :status="(reviewDetail.status as StatusMap)" class="mx-[16px]" /> <statusTag :status="(reviewDetail.status as ReviewStatus)" class="mx-[16px]" />
<MsPrevNextButton
ref="prevNextButtonRef"
v-model:loading="loading"
:page-change="pageChange"
:pagination="pagination"
:get-detail-func="getDetailFunc"
:detail-id="route.query.id as string"
:detail-index="detailIndex"
:table-data="tableData"
@loaded="loaded"
/>
</template> </template>
<template #headerRight> <template #headerRight>
<div class="mr-[16px] flex items-center"> <div class="mr-[16px] flex items-center">
<a-switch v-model:model-value="onlyMine" size="small" class="mr-[8px]" /> <a-switch v-model:model-value="onlyMine" size="small" class="mr-[8px]" />
{{ t('caseManagement.caseReview.onlyMine') }} {{ t('caseManagement.caseReview.onlyMine') }}
</div> </div>
<MsButton type="button" status="default"> <MsButton type="button" status="default" @click="associateDrawerVisible = true">
<MsIcon type="icon-icon_link-record_outlined1" class="mr-[8px]" /> <MsIcon type="icon-icon_link-record_outlined1" class="mr-[8px]" />
{{ t('ms.case.associate.title') }} {{ t('ms.case.associate.title') }}
</MsButton> </MsButton>
<MsButton type="button" status="default"> <MsButton type="button" status="default" @click="editReview">
<MsIcon type="icon-icon_edit_outlined" class="mr-[8px]" /> <MsIcon type="icon-icon_edit_outlined" class="mr-[8px]" />
{{ t('common.edit') }} {{ t('common.edit') }}
</MsButton> </MsButton>
<MsButton type="button" status="default"> <MsButton type="button" status="default" @click="copyReview">
<MsIcon type="icon-icon_copy_outlined" class="mr-[8px]" /> <MsIcon type="icon-icon_copy_outlined" class="mr-[8px]" />
{{ t('common.copy') }} {{ t('common.copy') }}
</MsButton> </MsButton>
<MsButton type="button" status="default"> <MsButton type="button" status="default" :loading="followLoading" @click="toggleFollowReview">
<MsIcon type="icon-icon_collection_outlined" class="mr-[8px]" /> <MsIcon
{{ t('common.fork') }} :type="reviewDetail.followFlag ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'"
:class="`mr-[8px] ${reviewDetail.followFlag ? 'text-[rgb(var(--warning-6))]' : ''}`"
/>
{{ t(reviewDetail.followFlag ? 'common.forked' : 'common.fork') }}
</MsButton> </MsButton>
<MsTableMoreAction :list="moreAction"> <MsTableMoreAction :list="moreAction" @select="handleMoreSelect">
<MsButton type="button" status="default"> <MsButton type="button" status="default">
<MsIcon type="icon-icon_more_outlined" class="mr-[8px]" /> <MsIcon type="icon-icon_more_outlined" class="mr-[8px]" />
{{ t('common.more') }} {{ t('common.more') }}
@ -58,19 +54,17 @@
<div class="mb-[4px] flex items-center gap-[24px]"> <div class="mb-[4px] flex items-center gap-[24px]">
<div class="text-[var(--color-text-4)]"> <div class="text-[var(--color-text-4)]">
<span class="mr-[8px]">{{ t('caseManagement.caseReview.reviewedCase') }}</span> <span class="mr-[8px]">{{ t('caseManagement.caseReview.reviewedCase') }}</span>
<span v-if="reviewDetail.status === 0" class="text-[var(--color-text-1)]">-</span> <span v-if="reviewDetail.status === 'PREPARED'" class="text-[var(--color-text-1)]">-</span>
<span v-else> <span v-else>
<span class="text-[var(--color-text-1)]">{{ reviewDetail.reviewCount }}/</span <span class="text-[var(--color-text-1)]"> {{ reviewDetail.reviewedCount }}/ </span
>{{ reviewDetail.caseCount }} >{{ reviewDetail.caseCount }}
</span> </span>
</div> </div>
<div class="text-[var(--color-text-4)]"> <div class="text-[var(--color-text-4)]">
<span class="mr-[8px]">{{ t('caseManagement.caseReview.passRate') }}</span> <span class="mr-[8px]">{{ t('caseManagement.caseReview.passRate') }}</span>
<span v-if="reviewDetail.status === 0" class="text-[var(--color-text-1)]">-</span> <span v-if="reviewDetail.status === 'PREPARED'" class="text-[var(--color-text-1)]">-</span>
<span v-else> <span v-else>
<span class="text-[var(--color-text-1)]"> <span class="text-[var(--color-text-1)]"> {{ reviewDetail.passRate }}% </span>
{{ ((reviewDetail.reviewCount / reviewDetail.caseCount) * 100).toFixed(2) }}%
</span>
</span> </span>
</div> </div>
</div> </div>
@ -84,18 +78,39 @@
</a-tabs> </a-tabs>
</div> </div>
</MsCard> </MsCard>
<MsCard class="mt-[16px]" :special-height="180" simple has-breadcrumb no-content-padding> <MsCard :loading="loading" class="mt-[16px]" :special-height="180" simple has-breadcrumb no-content-padding>
<MsSplitBox> <MsSplitBox>
<template #first> <template #first>
<div class="p-[24px]"> <div class="p-[24px]">
<CaseTree ref="folderTreeRef" @folder-node-select="handleFolderNodeSelect" /> <CaseTree
ref="folderTreeRef"
:modules-count="modulesCount"
:selected-keys="selectedKeys"
@folder-node-select="handleFolderNodeSelect"
@init="initModuleTree"
/>
</div> </div>
</template> </template>
<template #second> <template #second>
<CaseTable :active-folder="activeFolderId"></CaseTable> <CaseTable
ref="caseTableRef"
:active-folder="activeFolderId"
:only-mine="onlyMine"
:review-pass-rule="reviewDetail.reviewPassRule"
:offspring-ids="offspringIds"
:module-tree="moduleTree"
@init="initModulesCount"
@refresh="handleRefresh"
></CaseTable>
</template> </template>
</MsSplitBox> </MsSplitBox>
</MsCard> </MsCard>
<AssociateDrawer
v-model:visible="associateDrawerVisible"
v-model:project="associateDrawerProject"
@success="writeAssociateCases"
/>
<deleteReviewModal v-model:visible="deleteModalVisible" :record="reviewDetail" @success="handleDeleteSuccess" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -103,6 +118,7 @@
* @description 功能测试-用例评审-评审详情 * @description 功能测试-用例评审-评审详情
*/ */
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import MsCard from '@/components/pure/ms-card/index.vue'; import MsCard from '@/components/pure/ms-card/index.vue';
@ -110,41 +126,163 @@
import MsSplitBox from '@/components/pure/ms-split-box/index.vue'; import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue'; import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types'; import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsPrevNextButton from '@/components/business/ms-prev-next-button/index.vue'; import AssociateDrawer from './components/create/associateDrawer.vue';
import CaseTable from './components/detail/caseTable.vue'; import CaseTable from './components/detail/caseTable.vue';
import CaseTree from './components/detail/caseTree.vue'; import CaseTree from './components/detail/caseTree.vue';
import deleteReviewModal from './components/index/deleteReviewModal.vue';
import passRateLine from './components/passRateLine.vue'; import passRateLine from './components/passRateLine.vue';
import statusTag, { StatusMap } from './components/statusTag.vue'; import statusTag from './components/statusTag.vue';
import {
associateReviewCase,
followReview,
getReviewDetail,
getReviewDetailModuleCount,
} from '@/api/modules/case-management/caseReview';
import { reviewDefaultDetail } from '@/config/apiTest';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import useUserStore from '@/store/modules/user';
import type {
BaseAssociateCaseRequest,
ReviewDetailCaseListQueryParams,
ReviewItem,
ReviewStatus,
} from '@/models/caseManagement/caseReview';
import type { ModuleTreeNode } from '@/models/projectManagement/file';
import { CaseManagementRouteEnum } from '@/enums/routeEnum';
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const userStore = useUserStore();
const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const loading = ref(false); const loading = ref(false);
const reviewDetail = ref({ const reviewDetail = ref<ReviewItem>({
name: '具体的用例评审的名称最大宽度260px,超过展示省略号啦', ...reviewDefaultDetail,
status: 2,
caseCount: 100,
passCount: 0,
failCount: 10,
reviewCount: 20,
reviewingCount: 25,
}); });
async function initDetail() {
try {
loading.value = true;
const res = await getReviewDetail(route.query.id as string);
reviewDetail.value = res;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
const onlyMine = ref(false); const onlyMine = ref(false);
const moreAction = ref<ActionsItem[]>([]);
const fullActions = [ const showTab = ref(0);
const tabList = ref([
{ {
label: t('caseManagement.caseReview.archive'), key: 0,
eventTag: 'archive', title: t('menu.caseManagement.featureCase'),
icon: 'icon-icon-draft',
}, },
]);
const modulesCount = ref<Record<string, any>>({});
async function getModuleCount(params: ReviewDetailCaseListQueryParams) {
try {
modulesCount.value = await getReviewDetailModuleCount({
...params,
viewFlag: onlyMine.value,
reviewId: route.query.id as string,
});
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
const folderTreeRef = ref<InstanceType<typeof CaseTree>>();
const activeFolderId = ref<string>('all');
const offspringIds = ref<string[]>([]);
const selectedKeys = computed({
get: () => [activeFolderId.value],
set: (val) => val,
});
function handleFolderNodeSelect(ids: string[], _offspringIds: string[]) {
[activeFolderId.value] = ids;
offspringIds.value = [..._offspringIds];
}
function initModulesCount(params: ReviewDetailCaseListQueryParams) {
getModuleCount(params);
}
const caseTableRef = ref<InstanceType<typeof CaseTable>>();
const associateDrawerVisible = ref(false);
const associateDrawerProject = ref('');
//
async function writeAssociateCases(params: BaseAssociateCaseRequest & { reviewers: string[] }) {
try {
loading.value = true;
await associateReviewCase({
reviewId: route.query.id as string,
projectId: appStore.currentProjectId,
reviewers: params.reviewers,
baseAssociateCaseRequest: params,
});
Message.success(t('caseManagement.caseReview.associateSuccess'));
initDetail();
folderTreeRef.value?.initModules();
caseTableRef.value?.searchCase();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
function editReview() {
router.push({
name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW_CREATE,
query: {
id: route.query.id,
},
});
}
function createCase() {
router.push({
name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE_DETAIL,
query: {
reviewId: route.query.id,
},
});
}
const deleteModalVisible = ref(false);
function handleDeleteSuccess() {
router.back();
}
const fullActions = [
// {
// label: t('caseManagement.caseReview.archive'),
// eventTag: 'archive',
// icon: 'icon-icon-draft',
// },
// {
// label: t('common.export'),
// eventTag: 'export',
// icon: 'icon-icon_upload_outlined',
// },
{ {
label: t('common.export'), label: t('caseManagement.caseReview.quickCreate'),
eventTag: 'export', eventTag: 'createCase',
icon: 'icon-icon_upload_outlined', icon: 'icon-icon_add_outlined-1',
}, },
{ {
label: t('caseManagement.caseReview.createTestPlan'), label: t('caseManagement.caseReview.createTestPlan'),
@ -161,52 +299,72 @@
danger: true, danger: true,
}, },
]; ];
const moreAction = computed(() => {
onBeforeMount(() => { if (reviewDetail.value.status === 'COMPLETED') {
if (reviewDetail.value.status === 2) { return [...fullActions];
moreAction.value = [...fullActions];
} else if (reviewDetail.value.status === 3) {
moreAction.value = fullActions.filter((e) => e.eventTag === 'delete');
} else {
moreAction.value = fullActions.filter((e) => e.eventTag !== 'archive');
} }
if (reviewDetail.value.status === 'ARCHIVED') {
return fullActions.filter((e) => e.eventTag === 'delete');
}
return fullActions.filter((e) => e.eventTag !== 'archive');
}); });
const showTab = ref(0); function handleMoreSelect(item: ActionsItem) {
const tabList = ref([ switch (item.eventTag) {
{ case 'createCase':
key: 0, createCase();
title: t('menu.caseManagement.featureCase'), break;
}, case 'delete':
]); deleteModalVisible.value = true;
break;
default:
break;
}
}
const pagination = ref({ const followLoading = ref(false);
current: 1, async function toggleFollowReview() {
pageSize: 10, try {
total: 0, followLoading.value = true;
await followReview({
userId: userStore.id || '',
caseReviewId: route.query.id as string,
});
Message.success(
reviewDetail.value.followFlag
? t('caseManagement.caseReview.unFollowSuccess')
: t('caseManagement.caseReview.followSuccess')
);
reviewDetail.value.followFlag = !reviewDetail.value.followFlag;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
followLoading.value = false;
}
}
function copyReview() {
router.push({
name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW_CREATE,
query: {
copyId: route.query.id,
},
});
}
function handleRefresh() {
initDetail();
}
const moduleTree = ref<ModuleTreeNode[]>([]);
function initModuleTree(tree: ModuleTreeNode[]) {
moduleTree.value = unref(tree);
}
onMounted(() => {
initDetail();
}); });
const detailIndex = ref(0);
const tableData = ref([]);
async function getDetailFunc() {
console.log('getDetailFunc');
}
async function pageChange() {
console.log('page');
}
function loaded(e: any) {
loading.value = false;
reviewDetail.value = e;
}
const folderTreeRef = ref<InstanceType<typeof CaseTree>>();
const activeFolderId = ref<string | number>('all');
function handleFolderNodeSelect(ids: (string | number)[]) {
[activeFolderId.value] = ids;
}
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -2,7 +2,7 @@
<MsCard simple no-content-padding> <MsCard simple no-content-padding>
<div class="flex items-center justify-between border-b border-[var(--color-text-n8)] p-[24px_24px_16px_24px]"> <div class="flex items-center justify-between border-b border-[var(--color-text-n8)] p-[24px_24px_16px_24px]">
<a-button type="primary" @click="goCreateReview">{{ t('caseManagement.caseReview.create') }}</a-button> <a-button type="primary" @click="goCreateReview">{{ t('caseManagement.caseReview.create') }}</a-button>
<a-radio-group v-model:model-value="showType" type="button" class="file-show-type" @change="changeShowType"> <a-radio-group v-model:model-value="showType" type="button" class="file-show-type">
<a-radio value="all">{{ t('common.all') }}</a-radio> <a-radio value="all">{{ t('common.all') }}</a-radio>
<a-radio value="reviewByMe">{{ t('caseManagement.caseReview.waitMyReview') }}</a-radio> <a-radio value="reviewByMe">{{ t('caseManagement.caseReview.waitMyReview') }}</a-radio>
<a-radio value="createByMe">{{ t('caseManagement.caseReview.myCreate') }}</a-radio> <a-radio value="createByMe">{{ t('caseManagement.caseReview.myCreate') }}</a-radio>
@ -12,11 +12,24 @@
<MsSplitBox> <MsSplitBox>
<template #first> <template #first>
<div class="px-[24px] py-[16px]"> <div class="px-[24px] py-[16px]">
<ModuleTree ref="folderTreeRef" @folder-node-select="handleFolderNodeSelect" @init="initModuleTree" /> <ModuleTree
ref="folderTreeRef"
:show-type="showType"
:modules-count="modulesCount"
@folder-node-select="handleFolderNodeSelect"
@init="initModuleTree"
/>
</div> </div>
</template> </template>
<template #second> <template #second>
<ReviewTable :active-folder="activeFolderId" :module-tree="moduleTree" @go-create="goCreateReview" /> <ReviewTable
:active-folder="activeFolderId"
:module-tree="moduleTree"
:show-type="showType"
:offspring-ids="offspringIds"
@go-create="goCreateReview"
@init="initModuleCount"
/>
</template> </template>
</MsSplitBox> </MsSplitBox>
</div> </div>
@ -34,8 +47,10 @@
import ModuleTree from './components/index/moduleTree.vue'; import ModuleTree from './components/index/moduleTree.vue';
import ReviewTable from './components/index/reviewTable.vue'; import ReviewTable from './components/index/reviewTable.vue';
import { reviewModuleCount } from '@/api/modules/case-management/caseReview';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { ReviewListQueryParams } from '@/models/caseManagement/caseReview';
import type { ModuleTreeNode } from '@/models/projectManagement/file'; import type { ModuleTreeNode } from '@/models/projectManagement/file';
import { CaseManagementRouteEnum } from '@/enums/routeEnum'; import { CaseManagementRouteEnum } from '@/enums/routeEnum';
@ -46,20 +61,29 @@
const showType = ref<ShowType>('all'); const showType = ref<ShowType>('all');
function changeShowType(val: string | number | boolean) {
console.log('changeShowType', val);
}
const folderTreeRef = ref<InstanceType<typeof ModuleTree>>(); const folderTreeRef = ref<InstanceType<typeof ModuleTree>>();
const activeFolderId = ref<string | number>('all'); const activeFolderId = ref<string>('all');
const offspringIds = ref<string[]>([]);
const moduleTree = ref<ModuleTreeNode[]>([]); const moduleTree = ref<ModuleTreeNode[]>([]);
const modulesCount = ref<Record<string, number>>({});
function initModuleTree(tree: ModuleTreeNode[]) { function initModuleTree(tree: ModuleTreeNode[]) {
moduleTree.value = unref(tree); moduleTree.value = unref(tree);
} }
function handleFolderNodeSelect(ids: (string | number)[]) { function handleFolderNodeSelect(ids: string[], _offspringIds: string[]) {
[activeFolderId.value] = ids; [activeFolderId.value] = ids;
offspringIds.value = [..._offspringIds];
}
async function initModuleCount(params: ReviewListQueryParams) {
try {
const res = await reviewModuleCount(params);
modulesCount.value = res;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
} }
function goCreateReview() { function goCreateReview() {

View File

@ -65,7 +65,7 @@ export default {
'caseManagement.caseReview.reviewNameRequired': 'Review name cannot be empty', 'caseManagement.caseReview.reviewNameRequired': 'Review name cannot be empty',
'caseManagement.caseReview.descPlaceholder': 'Please describe this review', 'caseManagement.caseReview.descPlaceholder': 'Please describe this review',
'caseManagement.caseReview.belongModule': 'Belonging module', 'caseManagement.caseReview.belongModule': 'Belonging module',
'caseManagement.caseReview.belongModulePlaceholder': 'Please select the module to which the use case belongs', 'caseManagement.caseReview.belongModulePlaceholder': 'Please select the module to which the review belongs',
'caseManagement.caseReview.reviewerPlaceholder': 'Please select a reviewer', 'caseManagement.caseReview.reviewerPlaceholder': 'Please select a reviewer',
'caseManagement.caseReview.defaultReviewer': 'Default reviewer', 'caseManagement.caseReview.defaultReviewer': 'Default reviewer',
'caseManagement.caseReview.defaultReviewerRequired': 'The default reviewer cannot be empty', 'caseManagement.caseReview.defaultReviewerRequired': 'The default reviewer cannot be empty',

View File

@ -60,7 +60,7 @@ export default {
'caseManagement.caseReview.reviewNameRequired': '评审名称不能为空', 'caseManagement.caseReview.reviewNameRequired': '评审名称不能为空',
'caseManagement.caseReview.descPlaceholder': '请对该评审进行描述', 'caseManagement.caseReview.descPlaceholder': '请对该评审进行描述',
'caseManagement.caseReview.belongModule': '所属模块', 'caseManagement.caseReview.belongModule': '所属模块',
'caseManagement.caseReview.belongModulePlaceholder': '请选择该用例所属模块', 'caseManagement.caseReview.belongModulePlaceholder': '请选择该评审所属模块',
'caseManagement.caseReview.reviewerPlaceholder': '请选择评审人', 'caseManagement.caseReview.reviewerPlaceholder': '请选择评审人',
'caseManagement.caseReview.defaultReviewer': '默认评审人', 'caseManagement.caseReview.defaultReviewer': '默认评审人',
'caseManagement.caseReview.defaultReviewerRequired': '默认评审人', 'caseManagement.caseReview.defaultReviewerRequired': '默认评审人',
@ -122,4 +122,13 @@ export default {
'caseManagement.caseReview.crateCase': '创建用例', 'caseManagement.caseReview.crateCase': '创建用例',
'caseManagement.caseReview.demandCases': '需求关联列表', 'caseManagement.caseReview.demandCases': '需求关联列表',
'caseManagement.caseReview.demandSearchPlaceholder': '通过名称搜索', 'caseManagement.caseReview.demandSearchPlaceholder': '通过名称搜索',
'caseManagement.caseReview.quickCreate': '快捷创建',
'caseManagement.caseReview.followSuccess': '关注成功',
'caseManagement.caseReview.unFollowSuccess': '取消关注成功',
'caseManagement.caseReview.disassociateSuccess': '取消关联成功',
'caseManagement.caseReview.startTime': '开始时间',
'caseManagement.caseReview.endTime': '结束时间',
'caseManagement.caseReview.associateSuccess': '关联成功',
'caseManagement.caseReview.reviewSuccess': '评审成功',
'caseManagement.caseReview.updateCase': '更新用例',
}; };

View File

@ -68,6 +68,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onBeforeMount, ref, watch } from 'vue'; import { computed, onBeforeMount, ref, watch } from 'vue';
import { useVModel } from '@vueuse/core';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
@ -88,7 +89,7 @@
const props = defineProps<{ const props = defineProps<{
isExpandAll: boolean; isExpandAll: boolean;
activeFolder?: string; // 使 activeFolder?: string; // 使
selectedKeys?: Array<string | number>; // key selectedKeys: Array<string | number>; // key
isModal?: boolean; // isModal?: boolean; //
modulesCount?: Record<string, number>; // modulesCount?: Record<string, number>; //
showType?: string; // showType?: string; //
@ -131,21 +132,7 @@
]; ];
const renamePopVisible = ref(false); const renamePopVisible = ref(false);
const selectedKeys = ref(props.selectedKeys || []); const selectedKeys = useVModel(props, 'selectedKeys', emit);
watch(
() => props.selectedKeys,
(val) => {
selectedKeys.value = val || [];
}
);
watch(
() => selectedKeys.value,
(val) => {
emit('update:selectedKeys', val);
}
);
/** /**
* 初始化模块树 * 初始化模块树

View File

@ -11,7 +11,6 @@
allow-clear allow-clear
class="mr-[8px] w-[240px]" class="mr-[8px] w-[240px]"
:prefix="t('project.messageManagement.robot')" :prefix="t('project.messageManagement.robot')"
value-key="id"
:multiple="true" :multiple="true"
:has-all-select="true" :has-all-select="true"
:default-all-select="true" :default-all-select="true"
@ -77,6 +76,7 @@
label: (val as Record<string, any>).name, label: (val as Record<string, any>).name,
value: val, value: val,
})" })"
:object-value="true"
@remove="changeMessageReceivers(false, record, dataIndex as string)" @remove="changeMessageReceivers(false, record, dataIndex as string)"
@popup-visible-change="changeMessageReceivers($event, record, dataIndex as string)" @popup-visible-change="changeMessageReceivers($event, record, dataIndex as string)"
/> />
@ -148,7 +148,7 @@
const appStore = useAppStore(); const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const robotFilters = ref<RobotItem[]>([]); const robotFilters = ref<string[]>([]);
const robotOptions = ref<(SelectOptionData & RobotItem)[]>([]); const robotOptions = ref<(SelectOptionData & RobotItem)[]>([]);
const fullRef = ref<HTMLElement | null>(); const fullRef = ref<HTMLElement | null>();
@ -193,7 +193,7 @@
} }
const tempArr = [...staticColumns]; const tempArr = [...staticColumns];
for (let i = 0; i < robotFilters.value.length; i++) { for (let i = 0; i < robotFilters.value.length; i++) {
const robotId = robotFilters.value[i].id; const robotId = robotFilters.value[i];
tempArr.push({ tempArr.push({
title: robotOptions.value.find((e) => e.id === robotId)?.label, title: robotOptions.value.find((e) => e.id === robotId)?.label,
dataIndex: robotId, dataIndex: robotId,
@ -325,6 +325,7 @@
.filter((e) => e.enable) .filter((e) => e.enable)
.map((e) => ({ .map((e) => ({
label: e.name, label: e.name,
value: e.id,
...e, ...e,
})); }));
} }

View File

@ -568,4 +568,3 @@
} }
} }
</style> </style>
@/components/business/ms-select/index