feat(接口测试): 接口调试-部分 tab&用例评审部分接口联调&部分组件调整
This commit is contained in:
parent
f227f97679
commit
f6fb6d384b
|
@ -3,31 +3,52 @@ import {
|
|||
AddReviewModuleUrl,
|
||||
AddReviewUrl,
|
||||
AssociateReviewUrl,
|
||||
BatchChangeReviewerUrl,
|
||||
BatchDisassociateReviewCaseUrl,
|
||||
BatchReviewUrl,
|
||||
CopyReviewUrl,
|
||||
DeleteReviewModuleUrl,
|
||||
DeleteReviewUrl,
|
||||
DisassociateReviewCaseUrl,
|
||||
EditReviewUrl,
|
||||
FollowReviewUrl,
|
||||
GetAssociatedIdsUrl,
|
||||
GetCaseReviewHistoryListUrl,
|
||||
GetReviewDetailCasePageUrl,
|
||||
GetReviewDetailModuleCountUrl,
|
||||
GetReviewDetailModuleTreeUrl,
|
||||
GetReviewDetailUrl,
|
||||
GetReviewListUrl,
|
||||
GetReviewModulesUrl,
|
||||
GetReviewUsersUrl,
|
||||
MoveReviewModuleUrl,
|
||||
MoveReviewUrl,
|
||||
ReviewModuleCountUrl,
|
||||
SaveCaseReviewResultUrl,
|
||||
SortReviewDetailCaseUrl,
|
||||
SortReviewUrl,
|
||||
UpdateReviewModuleUrl,
|
||||
} from '@/api/requrls/case-management/caseReview';
|
||||
|
||||
import {
|
||||
AssociateReviewCaseParams,
|
||||
BatchCancelReviewCaseParams,
|
||||
BatchChangeReviewerParams,
|
||||
BatchMoveReviewParams,
|
||||
BatchReviewCaseParams,
|
||||
CommitReviewResultParams,
|
||||
CopyReviewParams,
|
||||
FollowReviewParams,
|
||||
Review,
|
||||
ReviewCaseItem,
|
||||
ReviewDetailCaseListQueryParams,
|
||||
ReviewHistoryItem,
|
||||
ReviewItem,
|
||||
ReviewListQueryParams,
|
||||
ReviewModule,
|
||||
ReviewModuleItem,
|
||||
ReviewUserItem,
|
||||
SortReviewCaseParams,
|
||||
SortReviewParams,
|
||||
UpdateReviewModuleParams,
|
||||
UpdateReviewParams,
|
||||
|
@ -59,6 +80,11 @@ export const deleteReviewModule = (id: string) => {
|
|||
return MSR.get({ url: DeleteReviewModuleUrl, params: id });
|
||||
};
|
||||
|
||||
// 评审模块树-统计用例数量
|
||||
export const reviewModuleCount = (data: ReviewListQueryParams) => {
|
||||
return MSR.post({ url: ReviewModuleCountUrl, data });
|
||||
};
|
||||
|
||||
// 新增评审
|
||||
export const addReview = (data: Review) => {
|
||||
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 });
|
||||
};
|
||||
|
||||
|
@ -109,7 +135,62 @@ export const getReviewUsers = (projectId: string, keyword: string) => {
|
|||
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) => {
|
||||
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 });
|
||||
};
|
||||
|
|
|
@ -76,6 +76,7 @@ import type {
|
|||
CreateOrUpdateModule,
|
||||
DeleteCaseType,
|
||||
DemandItem,
|
||||
DetailCase,
|
||||
ModulesTreeType,
|
||||
OperationFile,
|
||||
UpdateModule,
|
||||
|
|
|
@ -11,15 +11,19 @@ import {
|
|||
GetInfoUrl,
|
||||
GetLocalConfigUrl,
|
||||
GetMenuListUrl,
|
||||
GetPlatformAccountUrl,
|
||||
GetPlatformUrl,
|
||||
GetPublicKeyUrl,
|
||||
isLoginUrl,
|
||||
LoginUrl,
|
||||
LogoutUrl,
|
||||
SavePlatformUrl,
|
||||
UpdateAPIKEYUrl,
|
||||
UpdateInfoUrl,
|
||||
UpdateLocalConfigUrl,
|
||||
UpdatePswUrl,
|
||||
ValidAPIKEYUrl,
|
||||
ValidatePlatformUrl,
|
||||
ValidLocalConfigUrl,
|
||||
} from '@/api/requrls/user';
|
||||
|
||||
|
@ -137,3 +141,23 @@ export function updateBaseInfo(data: UpdateBaseInfo) {
|
|||
export function updatePsw(data: UpdatePswParams) {
|
||||
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 });
|
||||
}
|
||||
|
|
|
@ -8,9 +8,21 @@ export const AssociateReviewUrl = '/case/review/associate'; // 关联用例
|
|||
export const AddReviewUrl = '/case/review/add'; // 新增评审
|
||||
export const GetReviewUsersUrl = '/case/review/user-option'; // 获取评审人员列表
|
||||
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 MoveReviewModuleUrl = '/case/review/module/move'; // 移动评审模块
|
||||
export const AddReviewModuleUrl = '/case/review/module/add'; // 新增评审模块
|
||||
export const GetReviewModulesUrl = '/case/review/module/tree'; // 获取评审模块树
|
||||
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 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'; // 评审详情-提交评审
|
||||
|
|
|
@ -19,3 +19,7 @@ export const AddAPIKEYUrl = '/user/api/key/add'; // 个人设置-生成 APIKEY
|
|||
export const UpdatePswUrl = '/personal/update-password'; // 个人信息-修改密码
|
||||
export const UpdateInfoUrl = '/personal/update-info'; // 个人信息-修改信息
|
||||
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'; // 个人信息-获取三方平台账号信息-插件信息
|
||||
|
|
|
@ -407,7 +407,7 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.arco-radio-checked {
|
||||
.arco-radio-checked:not(.arco-radio-disabled) {
|
||||
.arco-radio-icon {
|
||||
@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 **/
|
||||
.arco-message {
|
||||
|
|
|
@ -15,9 +15,9 @@
|
|||
class="ml-2 max-w-[100px]"
|
||||
:placeholder="t('caseManagement.featureCase.PleaseSelect')"
|
||||
>
|
||||
<a-option v-for="item of props?.moduleOptions" :key="item.value" :value="item.value">{{
|
||||
t(item.label)
|
||||
}}</a-option>
|
||||
<a-option v-for="item of props?.moduleOptions" :key="item.value" :value="item.value">
|
||||
{{ t(item.label) }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -142,7 +142,7 @@
|
|||
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
|
||||
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 useAppStore from '@/store/modules/app';
|
||||
import { mapTree } from '@/utils';
|
||||
|
@ -164,7 +164,6 @@
|
|||
modulesParams?: Record<string, any>; // 获取模块树请求
|
||||
getTableFunc: (params: TableQueryParams) => Promise<CommonList<CaseManagementTable>>; // 获取表请求函数
|
||||
tableParams?: TableQueryParams; // 查询表格的额外的参数
|
||||
modulesCount: Record<string, any>; // 模块数量统计对象
|
||||
okButtonDisabled?: boolean; // 确认按钮是否禁用
|
||||
currentSelectCase: string | number | Record<string, any> | undefined; // 当前选中的用例类型
|
||||
moduleOptions?: { label: string; value: string }[]; // 功能模块对应用例下拉
|
||||
|
@ -178,7 +177,7 @@
|
|||
(e: 'update:currentSelectCase', val: string | number | Record<string, any> | undefined): void;
|
||||
(e: 'init', val: TableQueryParams): void; // 初始化模块数量
|
||||
(e: 'close'): void;
|
||||
(e: 'save', params: TableQueryParams): void; // 保存对外传递关联table 相关参数
|
||||
(e: 'save', params: any): void; // 保存对外传递关联table 相关参数
|
||||
}>();
|
||||
|
||||
const virtualListProps = computed(() => {
|
||||
|
@ -212,6 +211,7 @@
|
|||
|
||||
const protocolType = ref('HTTP'); // 协议类型
|
||||
const protocolOptions = ref(['HTTP']);
|
||||
const modulesCount = ref<Record<string, any>>({});
|
||||
|
||||
// 选中用例类型
|
||||
const caseType = computed({
|
||||
|
@ -248,7 +248,7 @@
|
|||
hideMoreAction: e.id === 'root',
|
||||
draggable: false,
|
||||
disabled: false,
|
||||
count: props.modulesCount?.[e.id] || 0,
|
||||
count: modulesCount.value[e.id] || 0,
|
||||
};
|
||||
});
|
||||
if (isSetDefaultKey) {
|
||||
|
@ -498,16 +498,22 @@
|
|||
}
|
||||
|
||||
// 初始化模块数量
|
||||
function initModuleCount() {
|
||||
emit('init', {
|
||||
keyword: keyword.value,
|
||||
moduleIds: [],
|
||||
projectId: innerProject.value,
|
||||
current: propsRes.value.msPagination?.current,
|
||||
pageSize: propsRes.value.msPagination?.pageSize,
|
||||
sourceId: props.caseId,
|
||||
combine: combine.value,
|
||||
});
|
||||
async function initModuleCount() {
|
||||
try {
|
||||
const params = {
|
||||
keyword: keyword.value,
|
||||
moduleIds: selectedModuleKeys.value,
|
||||
projectId: innerProject.value,
|
||||
current: propsRes.value.msPagination?.current,
|
||||
pageSize: propsRes.value.msPagination?.pageSize,
|
||||
combine: combine.value,
|
||||
};
|
||||
modulesCount.value = await getCaseModulesCounts(params);
|
||||
emit('init', params);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
function searchCase() {
|
||||
|
@ -556,6 +562,7 @@
|
|||
innerVisible.value = val;
|
||||
if (val) {
|
||||
resetSelector();
|
||||
initModules();
|
||||
searchCase();
|
||||
initFilter();
|
||||
}
|
||||
|
@ -567,7 +574,7 @@
|
|||
(val) => {
|
||||
emit('update:visible', val);
|
||||
if (val) {
|
||||
initModules(true);
|
||||
initModules();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -578,7 +585,7 @@
|
|||
(val) => {
|
||||
if (val) {
|
||||
emit('update:currentSelectCase', val);
|
||||
initModules(true);
|
||||
initModules();
|
||||
searchCase();
|
||||
}
|
||||
}
|
||||
|
@ -597,8 +604,12 @@
|
|||
() => innerProject.value,
|
||||
(val) => {
|
||||
emit('update:project', val);
|
||||
initModules(true);
|
||||
searchCase();
|
||||
if (innerVisible.value) {
|
||||
searchCase();
|
||||
resetSelector();
|
||||
initModules();
|
||||
searchCase();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -613,7 +624,7 @@
|
|||
* 初始化模块数量
|
||||
*/
|
||||
watch(
|
||||
() => props.modulesCount,
|
||||
() => modulesCount.value,
|
||||
(obj) => {
|
||||
folderTree.value = mapTree<ModuleTreeNode>(folderTree.value, (node) => {
|
||||
return {
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
|
||||
import MsAvatar from '@/components/pure/ms-avatar/index.vue';
|
||||
import MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue';
|
||||
|
@ -37,17 +38,12 @@
|
|||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:content', value: string): void;
|
||||
(event: 'publish', value: string): void;
|
||||
}>();
|
||||
|
||||
const isActive = ref(false);
|
||||
const currentContent = ref('');
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.content) {
|
||||
currentContent.value = props.content;
|
||||
}
|
||||
});
|
||||
const currentContent = useVModel(props, 'content', emit);
|
||||
|
||||
const publish = () => {
|
||||
emit('publish', currentContent.value);
|
||||
|
|
|
@ -4,163 +4,42 @@
|
|||
<div class="font-medium text-[var(--color-text-1)]">{{ t('ms.personal.tripartite') }}</div>
|
||||
</div>
|
||||
<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">
|
||||
<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)]">JIRA</div>
|
||||
<a-tooltip :content="t('ms.personal.jiraTip')" position="right">
|
||||
<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)]">{{ config.key }}</div>
|
||||
<a-tooltip v-if="config.tooltip" :content="config.tooltip" 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[jiraConfig.status].type" size="small" class="px-[4px]">
|
||||
{{ tagMap[jiraConfig.status].text }}
|
||||
<MsTag theme="light" :type="tagMap[config.status].type" size="small" class="px-[4px]">
|
||||
{{ tagMap[config.status].text }}
|
||||
</MsTag>
|
||||
</div>
|
||||
<a-form ref="jiraFormRef" :model="jiraConfig">
|
||||
<a-form-item :label="t('ms.personal.authType')">
|
||||
<a-radio-group v-model:model-value="jiraConfig.authType">
|
||||
<a-radio value="basic">Basic Auth</a-radio>
|
||||
<a-radio value="token">Bearer Token</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('ms.personal.platformAccount')">
|
||||
<a-input
|
||||
v-model:model-value="jiraConfig.platformAccount"
|
||||
: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>
|
||||
<MsFormCreate
|
||||
v-model:api="config.formModel"
|
||||
v-model:form-item="config.formItemList"
|
||||
:form-rule="config.formRules"
|
||||
:option="options"
|
||||
>
|
||||
</MsFormCreate>
|
||||
<a-button type="outline" :loading="config.validateLoading" @click="validate(config)">
|
||||
{{ t('ms.personal.valid') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 { getPlatform, getPlatformAccount, savePlatform, validatePlatform } from '@/api/modules/user/index';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
@ -185,33 +64,75 @@
|
|||
},
|
||||
};
|
||||
|
||||
const jiraConfig = ref({
|
||||
status: 0 as Status,
|
||||
authType: 'basic',
|
||||
platformAccount: '',
|
||||
platformPsw: '',
|
||||
const dynamicForm = ref<any>({});
|
||||
const options = ref({
|
||||
resetBtn: false,
|
||||
submitBtn: false,
|
||||
on: false,
|
||||
form: {
|
||||
layout: 'vertical',
|
||||
labelAlign: 'left',
|
||||
},
|
||||
row: {
|
||||
gutter: 0,
|
||||
},
|
||||
wrap: {
|
||||
'asterisk-position': 'end',
|
||||
'validate-trigger': ['change'],
|
||||
},
|
||||
});
|
||||
|
||||
const zendaoConfig = ref({
|
||||
status: 0 as Status,
|
||||
platformAccount: '',
|
||||
platformPsw: '',
|
||||
});
|
||||
async function initPlatformAccountInfo() {
|
||||
try {
|
||||
const res = await getPlatformAccount();
|
||||
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({
|
||||
status: 0 as Status,
|
||||
token: '',
|
||||
});
|
||||
async function initPlatformInfo() {
|
||||
try {
|
||||
const res = await getPlatform();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
const tapdConfig = ref({
|
||||
status: 0 as Status,
|
||||
name: '',
|
||||
async function validate(config: any) {
|
||||
try {
|
||||
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>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.platform-card-container {
|
||||
@apply flex flex-1 flex-wrap overflow-auto;
|
||||
@apply flex flex-wrap overflow-auto;
|
||||
.ms-scroll-bar();
|
||||
|
||||
padding: 16px;
|
||||
|
|
|
@ -26,6 +26,7 @@ export interface MsSearchSelectProps {
|
|||
valueKey?: string; // 选项的 value 字段名,默认为 value
|
||||
labelKey?: string; // 选项的 label 字段名,默认为 label
|
||||
options: SelectOptionData[];
|
||||
objectValue?: boolean; // 是否使用选项对象作为 value
|
||||
multiple?: boolean; // 是否多选
|
||||
atLeastOne?: boolean; // 是否至少选择一个,多选模式下有效
|
||||
remoteFieldsMap?: RemoteFieldsMap; // 远程模式下的结果 key 映射,例如 { value: 'id' },表示远程请求时,会将返回结果的 id 赋值到 value 字段
|
||||
|
@ -217,7 +218,9 @@ export default defineComponent(
|
|||
function handleSelectAllChange(val: boolean) {
|
||||
isSelectAll.value = 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);
|
||||
} else {
|
||||
innerValue.value = [];
|
||||
|
@ -256,7 +259,7 @@ export default defineComponent(
|
|||
<a-tooltip content={item.tooltipContent} mouse-enter-delay={500}>
|
||||
<a-option
|
||||
key={item[props.valueKey || 'value']}
|
||||
value={item}
|
||||
value={props.objectValue ? item : item[props.valueKey || 'value']}
|
||||
tag-props={
|
||||
props.multiple && props.atLeastOne
|
||||
? { closable: Array.isArray(innerValue.value) && innerValue.value.length > 1 }
|
||||
|
@ -375,7 +378,7 @@ export default defineComponent(
|
|||
class="one-line-text"
|
||||
style={singleTagMaxWidth.value > 0 ? { maxWidth: `${singleTagMaxWidth.value}px` } : {}}
|
||||
>
|
||||
{data.label}
|
||||
{slots.label ? slots.label(data) : data.label}
|
||||
</div>
|
||||
</a-tooltip>
|
||||
),
|
||||
|
@ -411,6 +414,7 @@ export default defineComponent(
|
|||
'fallbackOption',
|
||||
'labelKey',
|
||||
'atLeastOne',
|
||||
'objectValue',
|
||||
],
|
||||
emits: ['update:modelValue', 'remoteSearch', 'popupVisibleChange', 'update:loading', 'remove'],
|
||||
}
|
||||
|
|
|
@ -235,6 +235,7 @@
|
|||
(e: 'dataIndexChange', value: string): void;
|
||||
(e: 'update:count', value: number): void; // 用于展示 FilterIcon 的数量
|
||||
(e: 'update:rowCount', value: number): void; // 用于展示 MsBaseTable 的总行数
|
||||
(e: 'reset'): void;
|
||||
}>();
|
||||
|
||||
const isMultipleSelect = (dataIndex: string) => {
|
||||
|
@ -316,6 +317,7 @@
|
|||
const handleReset = () => {
|
||||
formRef.value?.resetFields();
|
||||
formModel.list = [getInitItem()];
|
||||
emit('reset');
|
||||
};
|
||||
/**
|
||||
* @description 筛选
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
class="mt-[8px]"
|
||||
@on-search="handleFilter"
|
||||
@data-index-change="dataIndexChange"
|
||||
@reset="emit('reset')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
@ -69,6 +70,7 @@
|
|||
(e: 'keywordSearch', value: string | undefined): void; // innerKeyword 搜索
|
||||
(e: 'advSearch', value: FilterResult): void; // 高级搜索
|
||||
(e: 'dataIndexChange', value: string): void; // 高级搜索选项变更
|
||||
(e: 'reset'): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
@ -82,7 +84,6 @@
|
|||
};
|
||||
|
||||
const handleFilter = (filter: FilterResult) => {
|
||||
console.log('filter', filter);
|
||||
emit('advSearch', filter);
|
||||
};
|
||||
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
type="icon-icon_that_person"
|
||||
:size="props.size"
|
||||
class="text-[var(--color-text-4)]"
|
||||
:style="{
|
||||
fontSize: `${props.size}px !important`,
|
||||
}"
|
||||
/>
|
||||
<a-avatar
|
||||
v-else-if="props.avatar === 'word'"
|
||||
|
|
|
@ -162,7 +162,7 @@
|
|||
const handleConfirm = async () => {
|
||||
await validateForm();
|
||||
if (props.isDelete) {
|
||||
emits('confirm');
|
||||
emits('confirm', undefined, handleCancel);
|
||||
} else {
|
||||
emits('confirm', form.value, handleCancel);
|
||||
}
|
||||
|
|
|
@ -85,6 +85,7 @@
|
|||
defineProps<{
|
||||
raw?: string;
|
||||
uploadImage?: (file: File) => Promise<any>;
|
||||
maxHeight?: string;
|
||||
}>(),
|
||||
{
|
||||
raw: '',
|
||||
|
@ -112,7 +113,6 @@
|
|||
const uploadQueue: queueAsPromised<Task> = fastq.promise(asyncWorker, 1);
|
||||
|
||||
const { currentLocale } = useLocale();
|
||||
const locale = computed(() => currentLocale.value as 'zh-CN' | 'en-US');
|
||||
|
||||
watch(
|
||||
() => props.raw,
|
||||
|
@ -359,20 +359,30 @@
|
|||
onBeforeUnmount(() => {
|
||||
editor.value?.destroy();
|
||||
});
|
||||
|
||||
const contentStyles = computed(() => {
|
||||
return {
|
||||
maxHeight: props.maxHeight || '200px',
|
||||
overflow: 'auto',
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rich-wrapper flex w-full">
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.rich-wrapper {
|
||||
position: relative;
|
||||
@apply relative overflow-hidden;
|
||||
|
||||
border: 1px solid var(--color-text-n8);
|
||||
border-radius: var(--border-radius-small);
|
||||
:deep(.halo-rich-text-editor .ProseMirror) {
|
||||
padding: 16px 24px !important;
|
||||
p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
@ -383,21 +393,22 @@
|
|||
color: var(--color-text-3) !important;
|
||||
}
|
||||
}
|
||||
// 修改滚动条
|
||||
:deep(.editor-header + div > div) {
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px !important;
|
||||
height: 4px !important;
|
||||
:deep(.editor-content) {
|
||||
.ms-scroll-bar();
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.v-popper__popper {
|
||||
.v-popper__inner {
|
||||
.drop-shadow {
|
||||
.ms-scroll-bar();
|
||||
}
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 8px !important;
|
||||
background: var(--color-text-input-border) !important;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: #a1a7b0 !important;
|
||||
}
|
||||
&&::-webkit-scrollbar-track {
|
||||
@apply bg-white !important;
|
||||
}
|
||||
.tippy-box {
|
||||
.command-items {
|
||||
.ms-scroll-bar();
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -150,6 +150,9 @@
|
|||
:deep(.arco-split-pane) {
|
||||
@apply relative overflow-hidden;
|
||||
}
|
||||
// :deep(.arco-split-pane-second) {
|
||||
// @apply z-10;
|
||||
// }
|
||||
.animating {
|
||||
:deep(.arco-split-pane) {
|
||||
@apply relative overflow-hidden;
|
||||
|
@ -207,12 +210,12 @@
|
|||
.vertical-expand-line {
|
||||
@apply relative z-10 flex items-center justify-center bg-transparent;
|
||||
&::before {
|
||||
@apply absolute w-full;
|
||||
@apply absolute w-full bg-transparent;
|
||||
|
||||
margin-bottom: -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),
|
||||
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 -4px 4px 0 rgb(255 255 255);
|
||||
content: '';
|
||||
}
|
||||
// .expand-icon--vertical {
|
||||
|
|
|
@ -94,6 +94,8 @@ export default function useTableProps<T>(
|
|||
const keyword = ref('');
|
||||
// 高级筛选
|
||||
const advanceFilter = reactive<FilterResult>({ accordBelow: 'AND', combine: {} });
|
||||
// 表格请求参数集合
|
||||
const tableQueryParams = ref<TableQueryParams>({});
|
||||
|
||||
// 是否分页
|
||||
if (propsRes.value.showPagination) {
|
||||
|
@ -193,7 +195,7 @@ export default function useTableProps<T>(
|
|||
try {
|
||||
if (loadListFunc) {
|
||||
setLoading(true);
|
||||
const data = await loadListFunc({
|
||||
tableQueryParams.value = {
|
||||
current,
|
||||
pageSize: currentPageSize,
|
||||
sort: sortItem.value,
|
||||
|
@ -202,7 +204,8 @@ export default function useTableProps<T>(
|
|||
combine: advanceFilter.combine,
|
||||
searchMode: advanceFilter.accordBelow,
|
||||
...loadListParams.value,
|
||||
});
|
||||
};
|
||||
const data = await loadListFunc(tableQueryParams.value);
|
||||
const tmpArr = data.list;
|
||||
propsRes.value.data = tmpArr.map((item: MsTableDataItem<T>) => {
|
||||
if (item.updateTime) {
|
||||
|
@ -312,6 +315,11 @@ export default function useTableProps<T>(
|
|||
}
|
||||
};
|
||||
|
||||
// 获取表格请求参数
|
||||
const getTableQueryParams = () => {
|
||||
return tableQueryParams.value;
|
||||
};
|
||||
|
||||
// 事件触发组
|
||||
const propsEvent = ref({
|
||||
// 排序触发
|
||||
|
@ -442,5 +450,6 @@ export default function useTableProps<T>(
|
|||
resetPagination,
|
||||
getSelectedCount,
|
||||
resetSelector,
|
||||
getTableQueryParams,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -222,7 +222,7 @@
|
|||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (props.isAllScreen) resizeObserver.value.disconnect();
|
||||
if (props.isAllScreen) resizeObserver.value?.disconnect();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<template v-if="showProjectSelect">
|
||||
<a-divider direction="vertical" class="ml-0" />
|
||||
<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"
|
||||
:bordered="false"
|
||||
allow-search
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -38,7 +38,7 @@ export default function useSelect(config: UseSelectOption) {
|
|||
let tagCount = 0;
|
||||
const values = Object.values(config.selectVal.value);
|
||||
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) {
|
||||
tagCount += 1;
|
||||
lastWidth -= tagWidth + 36; // 36px是标签的边距、边框等宽度
|
||||
|
|
|
@ -74,6 +74,7 @@ export default {
|
|||
'common.expandAll': 'Expand all',
|
||||
'common.copy': 'Copy',
|
||||
'common.fork': 'Fork',
|
||||
'common.forked': 'Forked',
|
||||
'common.more': 'More',
|
||||
'common.recycle': 'Recycle Bin',
|
||||
'common.new': 'New',
|
||||
|
@ -87,4 +88,6 @@ export default {
|
|||
'common.json': 'Object',
|
||||
'common.integer': 'Integer',
|
||||
'common.file': 'File',
|
||||
'common.desc': 'Description',
|
||||
'common.root': 'Default Module',
|
||||
};
|
||||
|
|
|
@ -60,7 +60,7 @@ async function changeLocale(locale: LocaleType) {
|
|||
|
||||
export default function useLocale() {
|
||||
const { locale } = i18n.global;
|
||||
const currentLocale = ref(locale);
|
||||
const currentLocale = ref(locale as LocaleType);
|
||||
|
||||
return {
|
||||
currentLocale,
|
||||
|
|
|
@ -72,10 +72,11 @@ export default {
|
|||
'common.quickAddMember': '快速添加成员',
|
||||
'common.export': '导出',
|
||||
'common.import': '导入',
|
||||
'common.collapseAll': '展开全部',
|
||||
'common.expandAll': '收起全部',
|
||||
'common.collapseAll': '收起全部',
|
||||
'common.expandAll': '展开全部',
|
||||
'common.copy': '复制',
|
||||
'common.fork': '关注',
|
||||
'common.forked': '已关注',
|
||||
'common.more': '更多',
|
||||
'common.recycle': '回收站',
|
||||
'common.new': '新增',
|
||||
|
@ -89,4 +90,6 @@ export default {
|
|||
'common.json': '对象',
|
||||
'common.integer': '整数',
|
||||
'common.file': '文件',
|
||||
'common.desc': '描述',
|
||||
'common.root': '默认模块',
|
||||
};
|
||||
|
|
|
@ -22,6 +22,17 @@ export interface ReviewModuleItem {
|
|||
}
|
||||
// 评审类型
|
||||
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 {
|
||||
projectId: string;
|
||||
|
@ -33,18 +44,22 @@ export interface Review {
|
|||
tags: string[];
|
||||
description: 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;
|
||||
}
|
||||
// 关联用例入参
|
||||
export interface AssociateReviewCaseParams {
|
||||
reviewId: string;
|
||||
projectId: string;
|
||||
caseIds: string[];
|
||||
reviewers: string[];
|
||||
baseAssociateCaseRequest: BaseAssociateCaseRequest;
|
||||
}
|
||||
// 关注/取消关注评审入参
|
||||
export interface FollowReviewParams {
|
||||
|
@ -66,12 +81,57 @@ export interface SortReviewParams {
|
|||
moveMode: ReviewMoveMode;
|
||||
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 {
|
||||
moduleIds: 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 {
|
||||
id: string;
|
||||
|
@ -86,13 +146,13 @@ export interface ReviewItem {
|
|||
endTime: number;
|
||||
caseCount: number;
|
||||
passRate: number;
|
||||
tags: string;
|
||||
tags: string[];
|
||||
description: string;
|
||||
createTime: number;
|
||||
createUser: string;
|
||||
updateTime: number;
|
||||
updateUser: string;
|
||||
reviewers: string[];
|
||||
reviewers: ReviewDetailReviewersItem[];
|
||||
passCount: number;
|
||||
unPassCount: number;
|
||||
reReviewedCount: number;
|
||||
|
@ -117,4 +177,44 @@ export interface ReviewUserItem {
|
|||
createUser: string;
|
||||
updateUser: string;
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { TableQueryParams } from '@/models/common';
|
||||
import { StatusType } from '@/enums/caseEnum';
|
||||
|
||||
import { ReviewResult } from './caseReview';
|
||||
|
||||
export interface ModulesTreeType {
|
||||
id: string;
|
||||
name: string;
|
||||
|
@ -148,7 +150,7 @@ export interface BatchMoveOrCopyType {
|
|||
excludeIds: string[] | undefined;
|
||||
condition: Record<string, any>;
|
||||
}
|
||||
|
||||
export type CaseEditType = 'STEP' | 'TEXT';
|
||||
// 创建或者更新
|
||||
export interface CreateOrUpdateCase {
|
||||
id?: string;
|
||||
|
@ -156,7 +158,7 @@ export interface CreateOrUpdateCase {
|
|||
templateId: string;
|
||||
name: string;
|
||||
prerequisite: string; // prerequisite
|
||||
caseEditType: string; // 编辑模式:步骤模式/文本模式
|
||||
caseEditType: CaseEditType; // 编辑模式:步骤模式/文本模式
|
||||
steps: string;
|
||||
textDescription: string;
|
||||
expectedResult: string; // 预期结果
|
||||
|
@ -191,8 +193,8 @@ export interface DetailCase {
|
|||
projectId: string;
|
||||
templateId?: string;
|
||||
name: string;
|
||||
reviewStatus?: string;
|
||||
tags: any;
|
||||
reviewStatus: ReviewResult;
|
||||
tags: string[];
|
||||
caseEditType: string;
|
||||
versionId?: string;
|
||||
publicCase: boolean;
|
||||
|
@ -206,6 +208,7 @@ export interface DetailCase {
|
|||
customFields: CustomAttributes[];
|
||||
attachments?: AttachFileInfo[];
|
||||
followFlag?: boolean;
|
||||
functionalPriority: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
|
|
|
@ -88,6 +88,7 @@ const CaseManagement: AppRouteRecordRaw = {
|
|||
isTopMenu: true,
|
||||
},
|
||||
},
|
||||
// 创建评审
|
||||
{
|
||||
path: 'caseManagementReviewCreate',
|
||||
name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW_CREATE,
|
||||
|
@ -109,6 +110,7 @@ const CaseManagement: AppRouteRecordRaw = {
|
|||
],
|
||||
},
|
||||
},
|
||||
// 评审详情
|
||||
{
|
||||
path: 'caseManagementReviewDetail',
|
||||
name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW_DETAIL,
|
||||
|
@ -128,6 +130,7 @@ const CaseManagement: AppRouteRecordRaw = {
|
|||
],
|
||||
},
|
||||
},
|
||||
// 评审详情-用例详情
|
||||
{
|
||||
path: 'caseManagementReviewDetailCaseDetail',
|
||||
name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW_DETAIL_CASE_DETAIL,
|
||||
|
|
|
@ -31,7 +31,7 @@ export interface AppRouteRecordRaw {
|
|||
component: Component | string;
|
||||
children?: AppRouteRecordRaw[];
|
||||
alias?: string | string[];
|
||||
props?: Record<string, any>;
|
||||
props?: Record<string, any> | boolean;
|
||||
beforeEnter?: NavigationGuard | NavigationGuard[];
|
||||
fullPath?: string;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<a-popover position="tl" :disabled="!props.desc || props.desc.trim() === ''" class="ms-params-input-popover">
|
||||
<template #content>
|
||||
<div class="param-popover-title">
|
||||
{{ t('ms.apiTestDebug.desc') }}
|
||||
{{ t('apiTestDebug.desc') }}
|
||||
</div>
|
||||
<div class="param-popover-value">
|
||||
{{ props.desc }}
|
||||
|
|
|
@ -2,15 +2,15 @@
|
|||
<MsBaseTable v-bind="propsRes" id="headerTable" :hoverable="false" v-on="propsEvent">
|
||||
<template #encodeTitle>
|
||||
<div class="flex items-center text-[var(--color-text-3)]">
|
||||
{{ t('ms.apiTestDebug.encode') }}
|
||||
{{ t('apiTestDebug.encode') }}
|
||||
<a-tooltip>
|
||||
<icon-question-circle
|
||||
class="ml-[4px] text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]"
|
||||
size="16"
|
||||
/>
|
||||
<template #content>
|
||||
<div>{{ t('ms.apiTestDebug.encodeTip1') }}</div>
|
||||
<div>{{ t('ms.apiTestDebug.encodeTip2') }}</div>
|
||||
<div>{{ t('apiTestDebug.encodeTip1') }}</div>
|
||||
<div>{{ t('apiTestDebug.encodeTip2') }}</div>
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
|
@ -19,7 +19,7 @@
|
|||
<a-popover position="tl" :disabled="!record.name || record.name.trim() === ''" class="ms-params-input-popover">
|
||||
<template #content>
|
||||
<div class="param-popover-title">
|
||||
{{ t('ms.apiTestDebug.paramName') }}
|
||||
{{ t('apiTestDebug.paramName') }}
|
||||
</div>
|
||||
<div class="param-popover-value">
|
||||
{{ record.name }}
|
||||
|
@ -27,14 +27,14 @@
|
|||
</template>
|
||||
<a-input
|
||||
v-model:model-value="record.name"
|
||||
:placeholder="t('ms.apiTestDebug.paramNamePlaceholder')"
|
||||
:placeholder="t('apiTestDebug.paramNamePlaceholder')"
|
||||
class="param-input"
|
||||
@input="(val) => addTableLine(val)"
|
||||
/>
|
||||
</a-popover>
|
||||
</template>
|
||||
<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
|
||||
type="icon"
|
||||
:class="[
|
||||
|
@ -65,14 +65,14 @@
|
|||
<div class="flex items-center justify-between">
|
||||
<a-input-number
|
||||
v-model:model-value="record.min"
|
||||
:placeholder="t('ms.apiTestDebug.paramMin')"
|
||||
:placeholder="t('apiTestDebug.paramMin')"
|
||||
class="param-input"
|
||||
@input="(val) => addTableLine(val)"
|
||||
></a-input-number>
|
||||
<div class="mx-[4px]">~</div>
|
||||
<a-input-number
|
||||
v-model:model-value="record.max"
|
||||
:placeholder="t('ms.apiTestDebug.paramMax')"
|
||||
:placeholder="t('apiTestDebug.paramMax')"
|
||||
class="param-input"
|
||||
@input="(val) => addTableLine(val)"
|
||||
></a-input-number>
|
||||
|
@ -127,7 +127,7 @@
|
|||
<a-modal
|
||||
v-model:visible="showQuickInputParam"
|
||||
:title="t('ms.paramsInput.value')"
|
||||
:ok-text="t('ms.apiTestDebug.apply')"
|
||||
:ok-text="t('apiTestDebug.apply')"
|
||||
class="ms-modal-form"
|
||||
body-class="!p-0"
|
||||
:width="680"
|
||||
|
@ -145,7 +145,7 @@
|
|||
<template #title>
|
||||
<div class="flex justify-between">
|
||||
<div class="text-[var(--color-text-1)]">
|
||||
{{ t('ms.apiTestDebug.quickInputParamsTip') }}
|
||||
{{ t('apiTestDebug.quickInputParamsTip') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -153,7 +153,7 @@
|
|||
</a-modal>
|
||||
<a-modal
|
||||
v-model:visible="showQuickInputDesc"
|
||||
:title="t('ms.apiTestDebug.desc')"
|
||||
:title="t('apiTestDebug.desc')"
|
||||
:ok-text="t('common.save')"
|
||||
:ok-button-props="{ disabled: !quickInputDescValue || quickInputDescValue.trim() === '' }"
|
||||
class="ms-modal-form"
|
||||
|
@ -166,7 +166,7 @@
|
|||
>
|
||||
<a-textarea
|
||||
v-model:model-value="quickInputDescValue"
|
||||
:placeholder="t('ms.apiTestDebug.descPlaceholder')"
|
||||
:placeholder="t('apiTestDebug.descPlaceholder')"
|
||||
:max-length="255"
|
||||
show-word-limit
|
||||
></a-textarea>
|
||||
|
@ -203,7 +203,7 @@
|
|||
const props = defineProps<{
|
||||
params: any[];
|
||||
columns: MsTableColumn;
|
||||
format?: RequestBodyFormat;
|
||||
format?: RequestBodyFormat | 'query' | 'rest';
|
||||
scroll?: {
|
||||
x?: number | string;
|
||||
y?: number | string;
|
||||
|
@ -257,7 +257,11 @@
|
|||
},
|
||||
];
|
||||
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;
|
||||
|
|
|
@ -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>
|
|
@ -1,12 +1,12 @@
|
|||
<template>
|
||||
<a-button type="outline" size="mini" @click="showBatchAddParamDrawer = true">
|
||||
{{ t('ms.apiTestDebug.batchAdd') }}
|
||||
{{ t('apiTestDebug.batchAdd') }}
|
||||
</a-button>
|
||||
<MsDrawer
|
||||
v-model:visible="showBatchAddParamDrawer"
|
||||
:title="t('common.batchAdd')"
|
||||
:width="680"
|
||||
:ok-text="t('ms.apiTestDebug.apply')"
|
||||
:ok-text="t('apiTestDebug.apply')"
|
||||
disabled-width-drag
|
||||
@confirm="applyBatchParams"
|
||||
>
|
||||
|
@ -22,10 +22,10 @@
|
|||
<template #title>
|
||||
<div class="flex flex-col">
|
||||
<div class="text-[12px] leading-[16px] text-[var(--color-text-4)]">
|
||||
{{ t('ms.apiTestDebug.batchAddParamsTip') }}
|
||||
{{ t('apiTestDebug.batchAddParamsTip') }}
|
||||
</div>
|
||||
<div class="text-[12px] leading-[16px] text-[var(--color-text-4)]">
|
||||
{{ t('ms.apiTestDebug.batchAddParamsTip2') }}
|
||||
{{ t('apiTestDebug.batchAddParamsTip2') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<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]">
|
||||
<batchAddKeyVal v-if="showParamTable" :params="currentTableParams" @apply="handleBatchParamApply" />
|
||||
<a-radio-group v-model:model-value="format" type="button" size="small" @change="formatChange">
|
||||
|
@ -12,7 +12,7 @@
|
|||
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)]"
|
||||
>
|
||||
{{ t('ms.apiTestDebug.noneBody') }}
|
||||
{{ t('apiTestDebug.noneBody') }}
|
||||
</div>
|
||||
<paramTable
|
||||
v-else-if="showParamTable"
|
||||
|
@ -23,6 +23,30 @@
|
|||
:height-used="heightUsed"
|
||||
@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)]">
|
||||
<MsCodeEditor
|
||||
v-model:model-value="currentBodyCode"
|
||||
|
@ -35,10 +59,10 @@
|
|||
<template #title>
|
||||
<div class="flex flex-col">
|
||||
<div class="text-[12px] leading-[16px] text-[var(--color-text-4)]">
|
||||
{{ t('ms.apiTestDebug.batchAddParamsTip') }}
|
||||
{{ t('apiTestDebug.batchAddParamsTip') }}
|
||||
</div>
|
||||
<div class="text-[12px] leading-[16px] text-[var(--color-text-4)]">
|
||||
{{ t('ms.apiTestDebug.batchAddParamsTip2') }}
|
||||
{{ t('apiTestDebug.batchAddParamsTip2') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -65,6 +89,8 @@
|
|||
json: string;
|
||||
xml: string;
|
||||
binary: string;
|
||||
binaryDesc: string;
|
||||
binarySend: boolean;
|
||||
raw: string;
|
||||
}
|
||||
const props = defineProps<{
|
||||
|
@ -83,35 +109,35 @@
|
|||
|
||||
const columns: MsTableColumn = [
|
||||
{
|
||||
title: 'ms.apiTestDebug.paramName',
|
||||
title: 'apiTestDebug.paramName',
|
||||
dataIndex: 'name',
|
||||
slotName: 'name',
|
||||
},
|
||||
{
|
||||
title: 'ms.apiTestDebug.paramType',
|
||||
title: 'apiTestDebug.paramType',
|
||||
dataIndex: 'type',
|
||||
slotName: 'type',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: 'ms.apiTestDebug.paramValue',
|
||||
title: 'apiTestDebug.paramValue',
|
||||
dataIndex: 'value',
|
||||
slotName: 'value',
|
||||
width: 240,
|
||||
},
|
||||
{
|
||||
title: 'ms.apiTestDebug.paramLengthRange',
|
||||
title: 'apiTestDebug.paramLengthRange',
|
||||
dataIndex: 'lengthRange',
|
||||
slotName: 'lengthRange',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: 'ms.apiTestDebug.desc',
|
||||
title: 'apiTestDebug.desc',
|
||||
dataIndex: 'desc',
|
||||
slotName: 'desc',
|
||||
},
|
||||
{
|
||||
title: 'ms.apiTestDebug.encode',
|
||||
title: 'apiTestDebug.encode',
|
||||
dataIndex: 'encode',
|
||||
slotName: 'encode',
|
||||
titleSlotName: 'encodeTitle',
|
||||
|
@ -150,8 +176,10 @@
|
|||
|
||||
const format = ref(RequestBodyFormat.NONE);
|
||||
const showParamTable = computed(() => {
|
||||
// 仅当格式为FORM_DATA或X_WWW_FORM_URLENCODED时,显示参数表格
|
||||
return [RequestBodyFormat.FORM_DATA, RequestBodyFormat.X_WWW_FORM_URLENCODED].includes(format.value);
|
||||
});
|
||||
// 当前显示的参数表格数据
|
||||
const currentTableParams = computed({
|
||||
get() {
|
||||
if (format.value === RequestBodyFormat.FORM_DATA) {
|
||||
|
@ -167,6 +195,7 @@
|
|||
}
|
||||
},
|
||||
});
|
||||
// 当前显示的代码
|
||||
const currentBodyCode = computed({
|
||||
get() {
|
||||
if (format.value === RequestBodyFormat.JSON) {
|
||||
|
@ -187,6 +216,7 @@
|
|||
}
|
||||
},
|
||||
});
|
||||
// 当前代码编辑器的语言
|
||||
const currentCodeLanguage = computed(() => {
|
||||
if (format.value === RequestBodyFormat.JSON) {
|
||||
return 'json';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<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" />
|
||||
</div>
|
||||
<paramTable
|
||||
|
@ -37,17 +37,17 @@
|
|||
|
||||
const columns: MsTableColumn = [
|
||||
{
|
||||
title: 'ms.apiTestDebug.paramName',
|
||||
title: 'apiTestDebug.paramName',
|
||||
dataIndex: 'name',
|
||||
slotName: 'name',
|
||||
},
|
||||
{
|
||||
title: 'ms.apiTestDebug.paramValue',
|
||||
title: 'apiTestDebug.paramValue',
|
||||
dataIndex: 'value',
|
||||
slotName: 'value',
|
||||
},
|
||||
{
|
||||
title: 'ms.apiTestDebug.desc',
|
||||
title: 'apiTestDebug.desc',
|
||||
dataIndex: 'desc',
|
||||
slotName: 'desc',
|
||||
},
|
||||
|
|
|
@ -35,25 +35,25 @@
|
|||
</a-select>
|
||||
<a-input
|
||||
v-model:model-value="debugUrl"
|
||||
:placeholder="t('ms.apiTestDebug.urlPlaceholder')"
|
||||
:placeholder="t('apiTestDebug.urlPlaceholder')"
|
||||
@change="handleActiveDebugChange"
|
||||
/>
|
||||
</a-input-group>
|
||||
</div>
|
||||
<div class="ml-[16px]">
|
||||
<a-dropdown-button class="exec-btn">
|
||||
{{ t('ms.apiTestDebug.serverExec') }}
|
||||
{{ t('apiTestDebug.serverExec') }}
|
||||
<template #icon>
|
||||
<icon-down />
|
||||
</template>
|
||||
<template #content>
|
||||
<a-doption>{{ t('ms.apiTestDebug.localExec') }}</a-doption>
|
||||
<a-doption>{{ t('apiTestDebug.localExec') }}</a-doption>
|
||||
</template>
|
||||
</a-dropdown-button>
|
||||
<a-button type="secondary">
|
||||
<div class="flex items-center">
|
||||
{{ 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>
|
||||
</a-button>
|
||||
</div>
|
||||
|
@ -88,6 +88,30 @@
|
|||
:second-box-height="secondBoxHeight"
|
||||
@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>
|
||||
</template>
|
||||
<template #second>
|
||||
|
@ -106,18 +130,19 @@
|
|||
<icon-right :size="12" />
|
||||
</MsButton>
|
||||
</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
|
||||
v-model:model-value="activeLayout"
|
||||
type="button"
|
||||
size="small"
|
||||
@change="handleActiveLayoutChange"
|
||||
>
|
||||
<a-radio value="vertical">{{ t('ms.apiTestDebug.vertical') }}</a-radio>
|
||||
<a-radio value="horizontal">{{ t('ms.apiTestDebug.horizontal') }}</a-radio>
|
||||
<a-radio value="vertical">{{ t('apiTestDebug.vertical') }}</a-radio>
|
||||
<a-radio value="horizontal">{{ t('apiTestDebug.horizontal') }}</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-[16px]"></div>
|
||||
</template>
|
||||
</MsSplitBox>
|
||||
</div>
|
||||
|
@ -131,8 +156,12 @@
|
|||
import { TabItem } from '@/components/pure/ms-editable-tab/types';
|
||||
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
|
||||
import apiMethodName from '../../../components/apiMethodName.vue';
|
||||
import debugAuth from './auth.vue';
|
||||
import debugBody, { BodyParams } from './body.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 { registerCatchSaveShortcut, removeCatchSaveShortcut } from '@/utils/event';
|
||||
|
@ -150,6 +179,8 @@
|
|||
json: '',
|
||||
xml: '',
|
||||
binary: '',
|
||||
binaryDesc: '',
|
||||
binarySend: false,
|
||||
raw: '',
|
||||
};
|
||||
const debugTabs = ref<TabItem[]>([
|
||||
|
@ -157,12 +188,25 @@
|
|||
id: initDefaultId,
|
||||
moduleProtocol: 'http',
|
||||
activeTab: RequestComposition.HEADER,
|
||||
label: t('ms.apiTestDebug.newApi'),
|
||||
label: t('apiTestDebug.newApi'),
|
||||
closable: true,
|
||||
method: RequestMethods.GET,
|
||||
unSave: false,
|
||||
headerParams: [],
|
||||
bodyParams: cloneDeep(defaultBodyParams),
|
||||
queryParams: [],
|
||||
restParams: [],
|
||||
authParams: {
|
||||
authType: 'none',
|
||||
account: '',
|
||||
password: '',
|
||||
},
|
||||
setting: {
|
||||
connectTimeout: 60000,
|
||||
responseTimeout: 60000,
|
||||
certificateAlias: '',
|
||||
redirect: 'follow',
|
||||
},
|
||||
},
|
||||
]);
|
||||
const debugUrl = ref('');
|
||||
|
@ -182,22 +226,35 @@
|
|||
id,
|
||||
moduleProtocol: 'http',
|
||||
activeTab: RequestComposition.HEADER,
|
||||
label: t('ms.apiTestDebug.newApi'),
|
||||
label: t('apiTestDebug.newApi'),
|
||||
closable: true,
|
||||
method: RequestMethods.GET,
|
||||
unSave: false,
|
||||
headerParams: [],
|
||||
bodyParams: cloneDeep(defaultBodyParams),
|
||||
queryParams: [],
|
||||
restParams: [],
|
||||
authParams: {
|
||||
authType: 'none',
|
||||
account: '',
|
||||
password: '',
|
||||
},
|
||||
setting: {
|
||||
connectTimeout: 60000,
|
||||
responseTimeout: 60000,
|
||||
certificateAlias: '',
|
||||
redirect: 'follow',
|
||||
},
|
||||
});
|
||||
activeTab.value = id;
|
||||
}
|
||||
|
||||
function closeDebugTab(tab: TabItem) {
|
||||
const index = debugTabs.value.findIndex((item) => item.id === tab.id);
|
||||
debugTabs.value.splice(index, 1);
|
||||
if (activeTab.value === tab.id) {
|
||||
activeTab.value = debugTabs.value[0]?.id || '';
|
||||
}
|
||||
debugTabs.value.splice(index, 1);
|
||||
}
|
||||
|
||||
const moreActionList = [
|
||||
|
@ -214,11 +271,11 @@
|
|||
const contentTabList = [
|
||||
{
|
||||
value: RequestComposition.HEADER,
|
||||
label: t('ms.apiTestDebug.header'),
|
||||
label: t('apiTestDebug.header'),
|
||||
},
|
||||
{
|
||||
value: RequestComposition.BODY,
|
||||
label: t('ms.apiTestDebug.body'),
|
||||
label: t('apiTestDebug.body'),
|
||||
},
|
||||
{
|
||||
value: RequestComposition.QUERY,
|
||||
|
@ -230,23 +287,23 @@
|
|||
},
|
||||
{
|
||||
value: RequestComposition.PREFIX,
|
||||
label: t('ms.apiTestDebug.prefix'),
|
||||
label: t('apiTestDebug.prefix'),
|
||||
},
|
||||
{
|
||||
value: RequestComposition.POST_CONDITION,
|
||||
label: t('ms.apiTestDebug.postCondition'),
|
||||
label: t('apiTestDebug.postCondition'),
|
||||
},
|
||||
{
|
||||
value: RequestComposition.ASSERTION,
|
||||
label: t('ms.apiTestDebug.assertion'),
|
||||
label: t('apiTestDebug.assertion'),
|
||||
},
|
||||
{
|
||||
value: RequestComposition.AUTH,
|
||||
label: t('ms.apiTestDebug.auth'),
|
||||
label: t('apiTestDebug.auth'),
|
||||
},
|
||||
{
|
||||
value: RequestComposition.SETTING,
|
||||
label: t('ms.apiTestDebug.setting'),
|
||||
label: t('apiTestDebug.setting'),
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -292,6 +349,7 @@
|
|||
function handleActiveLayoutChange() {
|
||||
isExpanded.value = true;
|
||||
splitBoxSize.value = 0.6;
|
||||
splitBoxRef.value?.expand(0.6);
|
||||
}
|
||||
|
||||
function saveDebug() {
|
||||
|
|
|
@ -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>
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<div> </div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -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>
|
|
@ -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>
|
|
@ -7,10 +7,10 @@
|
|||
allow-clear
|
||||
/>
|
||||
<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>
|
||||
<a-doption value="newApi">{{ t('ms.apiTestDebug.newApi') }}</a-doption>
|
||||
<a-doption value="import">{{ t('ms.apiTestDebug.importApi') }}</a-doption>
|
||||
<a-doption value="newApi">{{ t('apiTestDebug.newApi') }}</a-doption>
|
||||
<a-doption value="import">{{ t('apiTestDebug.importApi') }}</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
|
@ -46,7 +46,7 @@
|
|||
:node-more-actions="folderMoreActions"
|
||||
:default-expand-all="isExpandAll"
|
||||
:expand-all="isExpandAll"
|
||||
:empty-text="t('ms.apiTestDebug.noMatchModule')"
|
||||
:empty-text="t('apiTestDebug.noMatchModule')"
|
||||
:draggable="!props.isModal"
|
||||
:virtual-list-props="virtualListProps"
|
||||
:field-names="{
|
||||
|
|
|
@ -1,31 +1,31 @@
|
|||
export default {
|
||||
'ms.apiTestDebug.newApi': 'New request',
|
||||
'ms.apiTestDebug.importApi': 'Import request',
|
||||
'ms.apiTestDebug.urlPlaceholder': 'Please enter the full URL including http or https',
|
||||
'ms.apiTestDebug.serverExec': 'Server execution',
|
||||
'ms.apiTestDebug.localExec': 'Local execution',
|
||||
'ms.apiTestDebug.noMatchModule': 'No matching module data yet',
|
||||
'ms.apiTestDebug.header': 'Header',
|
||||
'ms.apiTestDebug.body': 'Body',
|
||||
'ms.apiTestDebug.prefix': 'Prefix',
|
||||
'ms.apiTestDebug.postCondition': 'Post condition',
|
||||
'ms.apiTestDebug.assertion': 'Assertion',
|
||||
'ms.apiTestDebug.auth': 'Auth',
|
||||
'ms.apiTestDebug.setting': 'Setting',
|
||||
'ms.apiTestDebug.batchAdd': 'Batch add',
|
||||
'ms.apiTestDebug.responseContent': 'Response content',
|
||||
'ms.apiTestDebug.vertical': 'Vertical layout',
|
||||
'ms.apiTestDebug.horizontal': 'Horizontal layout',
|
||||
'ms.apiTestDebug.paramName': 'Parameter name',
|
||||
'ms.apiTestDebug.paramNamePlaceholder': 'Please enter parameter name',
|
||||
'ms.apiTestDebug.paramValue': 'Parameter value',
|
||||
'ms.apiTestDebug.paramValuePlaceholder': 'Starting with {at}, double-click to quickly enter',
|
||||
'ms.apiTestDebug.paramValuePreview': 'Parameter preview',
|
||||
'ms.apiTestDebug.desc': 'Description',
|
||||
'ms.apiTestDebug.apply': 'Apply',
|
||||
'ms.apiTestDebug.batchAddParamsTip': 'Writing format: parameter name: parameter value; such as nama: natural',
|
||||
'ms.apiTestDebug.batchAddParamsTip2':
|
||||
'apiTestDebug.newApi': 'New request',
|
||||
'apiTestDebug.importApi': 'Import request',
|
||||
'apiTestDebug.urlPlaceholder': 'Please enter the full URL including http or https',
|
||||
'apiTestDebug.serverExec': 'Server execution',
|
||||
'apiTestDebug.localExec': 'Local execution',
|
||||
'apiTestDebug.noMatchModule': 'No matching module data yet',
|
||||
'apiTestDebug.header': 'Header',
|
||||
'apiTestDebug.body': 'Body',
|
||||
'apiTestDebug.prefix': 'Prefix',
|
||||
'apiTestDebug.postCondition': 'Post condition',
|
||||
'apiTestDebug.assertion': 'Assertion',
|
||||
'apiTestDebug.auth': 'Auth',
|
||||
'apiTestDebug.setting': 'Setting',
|
||||
'apiTestDebug.batchAdd': 'Batch add',
|
||||
'apiTestDebug.responseContent': 'Response content',
|
||||
'apiTestDebug.vertical': 'Vertical layout',
|
||||
'apiTestDebug.horizontal': 'Horizontal layout',
|
||||
'apiTestDebug.paramName': 'Parameter name',
|
||||
'apiTestDebug.paramNamePlaceholder': 'Please enter parameter name',
|
||||
'apiTestDebug.paramValue': 'Parameter value',
|
||||
'apiTestDebug.paramValuePlaceholder': 'Starting with {at}, double-click to quickly enter',
|
||||
'apiTestDebug.paramValuePreview': 'Parameter preview',
|
||||
'apiTestDebug.desc': 'Description',
|
||||
'apiTestDebug.apply': 'Apply',
|
||||
'apiTestDebug.batchAddParamsTip': 'Writing format: parameter name: parameter value; such as nama: natural',
|
||||
'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.',
|
||||
'ms.apiTestDebug.quickInputParamsTip': 'Support Mock/JMeter/Json/Text/String, etc.',
|
||||
'ms.apiTestDebug.descPlaceholder': 'Please enter content',
|
||||
'apiTestDebug.quickInputParamsTip': 'Support Mock/JMeter/Json/Text/String, etc.',
|
||||
'apiTestDebug.descPlaceholder': 'Please enter content',
|
||||
};
|
||||
|
|
|
@ -1,40 +1,57 @@
|
|||
export default {
|
||||
'ms.apiTestDebug.newApi': '新建请求',
|
||||
'ms.apiTestDebug.importApi': '导入请求',
|
||||
'ms.apiTestDebug.urlPlaceholder': '请输入包含 http 或 https 的完整URL',
|
||||
'ms.apiTestDebug.serverExec': '服务端执行',
|
||||
'ms.apiTestDebug.localExec': '本地执行',
|
||||
'ms.apiTestDebug.noMatchModule': '暂无匹配的模块数据',
|
||||
'ms.apiTestDebug.header': '请求头',
|
||||
'ms.apiTestDebug.body': '请求体',
|
||||
'ms.apiTestDebug.prefix': '前置',
|
||||
'ms.apiTestDebug.postCondition': '后置',
|
||||
'ms.apiTestDebug.assertion': '断言',
|
||||
'ms.apiTestDebug.auth': '认证',
|
||||
'ms.apiTestDebug.setting': '设置',
|
||||
'ms.apiTestDebug.batchAdd': '批量添加',
|
||||
'ms.apiTestDebug.responseContent': '响应内容',
|
||||
'ms.apiTestDebug.vertical': '上下布局',
|
||||
'ms.apiTestDebug.horizontal': '左右布局',
|
||||
'ms.apiTestDebug.paramName': '参数名称',
|
||||
'ms.apiTestDebug.paramNamePlaceholder': '请输入参数名称',
|
||||
'ms.apiTestDebug.paramRequired': '必填',
|
||||
'ms.apiTestDebug.paramNotRequired': '非必填',
|
||||
'ms.apiTestDebug.paramType': '类型',
|
||||
'ms.apiTestDebug.paramValue': '参数值',
|
||||
'ms.apiTestDebug.paramValuePlaceholder': '以{at}开始,双击可快速输入',
|
||||
'ms.apiTestDebug.paramLengthRange': '长度区间',
|
||||
'ms.apiTestDebug.paramMin': '最小值',
|
||||
'ms.apiTestDebug.paramMax': '最大值',
|
||||
'ms.apiTestDebug.paramValuePreview': '参数预览',
|
||||
'ms.apiTestDebug.desc': '描述',
|
||||
'ms.apiTestDebug.encode': '编码',
|
||||
'ms.apiTestDebug.encodeTip1': '开启:使用编码',
|
||||
'ms.apiTestDebug.encodeTip2': '关闭:不使用编码',
|
||||
'ms.apiTestDebug.apply': '应用',
|
||||
'ms.apiTestDebug.batchAddParamsTip': '书写格式:参数名:参数值;如 nama:natural',
|
||||
'ms.apiTestDebug.batchAddParamsTip2': '注: 多条记录以换行分隔,批量添加里的参数名重复,默认以最后一条数据为最新数据',
|
||||
'ms.apiTestDebug.quickInputParamsTip': '支持Mock/JMeter/Json/Text/String等',
|
||||
'ms.apiTestDebug.descPlaceholder': '请输入内容',
|
||||
'ms.apiTestDebug.noneBody': '请求没有 Body',
|
||||
'apiTestDebug.newApi': '新建请求',
|
||||
'apiTestDebug.importApi': '导入请求',
|
||||
'apiTestDebug.urlPlaceholder': '请输入包含 http 或 https 的完整URL',
|
||||
'apiTestDebug.serverExec': '服务端执行',
|
||||
'apiTestDebug.localExec': '本地执行',
|
||||
'apiTestDebug.noMatchModule': '暂无匹配的模块数据',
|
||||
'apiTestDebug.header': '请求头',
|
||||
'apiTestDebug.body': '请求体',
|
||||
'apiTestDebug.prefix': '前置',
|
||||
'apiTestDebug.postCondition': '后置',
|
||||
'apiTestDebug.assertion': '断言',
|
||||
'apiTestDebug.auth': '认证',
|
||||
'apiTestDebug.setting': '设置',
|
||||
'apiTestDebug.batchAdd': '批量添加',
|
||||
'apiTestDebug.responseContent': '响应内容',
|
||||
'apiTestDebug.vertical': '上下布局',
|
||||
'apiTestDebug.horizontal': '左右布局',
|
||||
'apiTestDebug.paramName': '参数名称',
|
||||
'apiTestDebug.paramNamePlaceholder': '请输入参数名称',
|
||||
'apiTestDebug.paramRequired': '必填',
|
||||
'apiTestDebug.paramNotRequired': '非必填',
|
||||
'apiTestDebug.paramType': '类型',
|
||||
'apiTestDebug.paramValue': '参数值',
|
||||
'apiTestDebug.paramValuePlaceholder': '以{at}开始,双击可快速输入',
|
||||
'apiTestDebug.paramLengthRange': '长度区间',
|
||||
'apiTestDebug.paramMin': '最小值',
|
||||
'apiTestDebug.paramMax': '最大值',
|
||||
'apiTestDebug.paramValuePreview': '参数预览',
|
||||
'apiTestDebug.desc': '描述',
|
||||
'apiTestDebug.encode': '编码',
|
||||
'apiTestDebug.encodeTip1': '开启:使用编码',
|
||||
'apiTestDebug.encodeTip2': '关闭:不使用编码',
|
||||
'apiTestDebug.apply': '应用',
|
||||
'apiTestDebug.batchAddParamsTip': '书写格式:参数名:参数值;如 nama:natural',
|
||||
'apiTestDebug.batchAddParamsTip2': '注: 多条记录以换行分隔,批量添加里的参数名重复,默认以最后一条数据为最新数据',
|
||||
'apiTestDebug.quickInputParamsTip': '支持Mock/JMeter/Json/Text/String等',
|
||||
'apiTestDebug.descPlaceholder': '请输入内容',
|
||||
'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': '自动',
|
||||
};
|
||||
|
|
|
@ -281,10 +281,13 @@
|
|||
() => props.stepList,
|
||||
() => {
|
||||
stepData.value = props.stepList;
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
onBeforeMount(() => {
|
||||
setProps({ data: stepData.value });
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -7,7 +7,11 @@
|
|||
@save="saveHandler"
|
||||
@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>
|
||||
<div class="flex justify-end gap-[16px]">
|
||||
<a-button type="secondary" @click="cancelHandler">{{ t('mscard.defaultCancelText') }}</a-button>
|
||||
|
@ -38,6 +42,7 @@
|
|||
import useFeatureCaseStore from '@/store/modules/case/featureCase';
|
||||
import { scrollIntoView } from '@/utils/dom';
|
||||
|
||||
import { CreateOrUpdateCase } from '@/models/caseManagement/featureCase';
|
||||
import { CaseManagementRouteEnum } from '@/enums/routeEnum';
|
||||
|
||||
import Message from '@arco-design/web-vue/es/message';
|
||||
|
@ -51,8 +56,8 @@
|
|||
const visitedKey = 'doNotNextTipCreateCase';
|
||||
const { getIsVisited } = useVisit(visitedKey);
|
||||
|
||||
const caseDetailInfo = ref<Record<string, any>>({
|
||||
request: {},
|
||||
const caseDetailInfo = ref({
|
||||
request: {} as CreateOrUpdateCase,
|
||||
fileList: [],
|
||||
});
|
||||
|
||||
|
@ -76,11 +81,10 @@
|
|||
query: { organizationId: route.query.organizationId, projectId: route.query.projectId },
|
||||
});
|
||||
} else {
|
||||
const res = await createCaseRequest(caseDetailInfo.value);
|
||||
if (isReview) {
|
||||
// TODO
|
||||
// 创建并关联接口
|
||||
caseDetailInfo.value.request.reviewId = route.query.reviewId;
|
||||
}
|
||||
const res = await createCaseRequest(caseDetailInfo.value);
|
||||
createSuccessId.value = res.data.id;
|
||||
Message.success(route.params.mode === 'copy' ? t('ms.description.copySuccess') : t('common.addSuccess'));
|
||||
featureCaseStore.setIsAlreadySuccess(true);
|
||||
|
@ -96,16 +100,24 @@
|
|||
}
|
||||
}
|
||||
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({
|
||||
name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW_DETAIL,
|
||||
name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE_CREATE_SUCCESS,
|
||||
query: {
|
||||
id: route.query.reviewId,
|
||||
organizationId: route.query.organizationId,
|
||||
projectId: route.query.projectId,
|
||||
id: createSuccessId.value,
|
||||
...route.query,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
|
|
|
@ -295,6 +295,8 @@
|
|||
tags: [],
|
||||
customFields: [], // 自定义字段集合
|
||||
relateFileMetaIds: [], // 关联文件ID集合
|
||||
functionalPriority: '',
|
||||
reviewStatus: 'UN_REVIEWED',
|
||||
};
|
||||
|
||||
const detailInfo = ref<DetailCase>({ ...initDetail });
|
||||
|
|
|
@ -91,9 +91,9 @@
|
|||
:request-fun="transferFileRequest"
|
||||
:params="{
|
||||
projectId: currentProjectId,
|
||||
caseId:route.query.id as string,
|
||||
fileId:item.uid,
|
||||
local:true
|
||||
caseId: props.caseId,
|
||||
fileId: item.uid,
|
||||
local: true,
|
||||
}"
|
||||
@success="getCaseInfo()"
|
||||
/>
|
||||
|
@ -119,7 +119,7 @@
|
|||
{{ t('ms.upload.preview') }}
|
||||
</MsButton>
|
||||
<MsButton
|
||||
v-if="route.query.id"
|
||||
v-if="props.caseId"
|
||||
type="button"
|
||||
status="primary"
|
||||
class="!mr-[4px]"
|
||||
|
@ -128,7 +128,7 @@
|
|||
{{ t('caseManagement.featureCase.download') }}
|
||||
</MsButton>
|
||||
<MsButton
|
||||
v-if="route.query.id && item.isUpdateFlag"
|
||||
v-if="props.caseId && item.isUpdateFlag"
|
||||
type="button"
|
||||
status="primary"
|
||||
@click="handleUpdateFile(item)"
|
||||
|
@ -278,6 +278,7 @@
|
|||
|
||||
const props = defineProps<{
|
||||
formModeValue: Record<string, any>; // 表单值
|
||||
caseId: string; // 用例id
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:formModeValue', 'changeFile']);
|
||||
|
@ -327,6 +328,8 @@
|
|||
tags: [],
|
||||
customFields: [],
|
||||
relateFileMetaIds: [],
|
||||
functionalPriority: '',
|
||||
reviewStatus: 'UN_REVIEWED',
|
||||
};
|
||||
|
||||
const form = ref<DetailCase | CreateOrUpdateCase>({ ...initForm });
|
||||
|
@ -411,7 +414,7 @@
|
|||
fileList.value.push(...fileResultList);
|
||||
}
|
||||
|
||||
const isEditOrCopy = computed(() => !!route.query.id);
|
||||
const isEditOrCopy = computed(() => !!props.caseId);
|
||||
const attachmentsList = ref<AttachFileInfo[]>([]);
|
||||
|
||||
// 后台传过来的local文件的item列表
|
||||
|
@ -473,14 +476,14 @@
|
|||
if (item.status !== 'init') {
|
||||
const res = await previewFile({
|
||||
projectId: currentProjectId.value,
|
||||
caseId: route.query.id as string,
|
||||
caseId: props.caseId,
|
||||
fileId: item.uid,
|
||||
local: item.local,
|
||||
});
|
||||
const blob = new Blob([res], { type: 'image/jpeg' });
|
||||
imageUrl.value = URL.createObjectURL(blob);
|
||||
} else {
|
||||
imageUrl.value = item.url as string;
|
||||
imageUrl.value = item.url || '';
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
@ -532,7 +535,7 @@
|
|||
try {
|
||||
isLoading.value = true;
|
||||
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);
|
||||
if (fileIds.length) {
|
||||
checkUpdateFileIds.value = await checkFileIsUpdateRequest(fileIds);
|
||||
|
@ -652,11 +655,11 @@
|
|||
type: item.type,
|
||||
name: item.id,
|
||||
label: item.name,
|
||||
value: isEditOrCopy.value ? JSON.parse(rule.value) : rule.value,
|
||||
value: rule.value,
|
||||
options: optionsItem,
|
||||
required: item.required,
|
||||
props: {
|
||||
modelValue: isEditOrCopy.value ? JSON.parse(rule.value) : rule.value,
|
||||
modelValue: rule.value,
|
||||
options: optionsItem,
|
||||
},
|
||||
};
|
||||
|
@ -707,7 +710,7 @@
|
|||
try {
|
||||
const res = await downloadFileRequest({
|
||||
projectId: currentProjectId.value,
|
||||
caseId: route.query.id as string,
|
||||
caseId: props.caseId,
|
||||
fileId: item.uid,
|
||||
local: true,
|
||||
});
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
<div class="absolute left-16 top-0 font-normal">
|
||||
<a-divider direction="vertical" />
|
||||
<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
|
||||
/></span>
|
||||
<template #content>
|
||||
|
@ -555,8 +555,8 @@
|
|||
|
||||
watch(
|
||||
() => props.form,
|
||||
() => {
|
||||
detailForm.value = { ...props.form };
|
||||
(val) => {
|
||||
detailForm.value = { ...val };
|
||||
getDetails();
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
<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>
|
||||
<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)]">
|
||||
{{ reviewName }}
|
||||
{{ reviewDetail.name }}
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<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))]"
|
||||
>
|
||||
<MsIcon type="icon-icon-contacts" size="13" />
|
||||
{{ t('caseManagement.caseReview.single') }}
|
||||
{{
|
||||
reviewDetail.reviewPassRule === 'SINGLE'
|
||||
? t('caseManagement.caseReview.single')
|
||||
: t('caseManagement.caseReview.multi')
|
||||
}}
|
||||
</div>
|
||||
<div class="ml-[16px] flex items-center">
|
||||
<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="h-full w-[356px] border-r border-[var(--color-text-n8)] pr-[16px] pt-[16px]">
|
||||
<div class="mb-[16px] flex">
|
||||
<a-input
|
||||
<a-input-search
|
||||
v-model:model-value="keyword"
|
||||
:placeholder="t('caseManagement.caseReview.searchPlaceholder')"
|
||||
allow-clear
|
||||
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 class="case-list">
|
||||
<div
|
||||
v-for="item of caseList"
|
||||
:key="item.id"
|
||||
:class="['case-item', activeCase.id === item.id ? 'case-item--active' : '']"
|
||||
@click="changeActiveCase(item)"
|
||||
>
|
||||
<div class="mb-[4px] flex items-center justify-between">
|
||||
<div>{{ item.id }}</div>
|
||||
<div class="flex items-center gap-[4px] leading-[22px]">
|
||||
<MsIcon
|
||||
:type="resultMap[item.result as ResultMap].icon"
|
||||
:style="{color: resultMap[item.result as ResultMap].color}"
|
||||
/>
|
||||
{{ t(resultMap[item.result as ResultMap].label) }}
|
||||
<a-spin :loading="caseListLoading" class="w-full">
|
||||
<div class="case-list">
|
||||
<div
|
||||
v-for="item of caseList"
|
||||
:key="item.caseId"
|
||||
:class="['case-item', caseDetail.id === item.caseId ? 'case-item--active' : '']"
|
||||
@click="changeActiveCase(item)"
|
||||
>
|
||||
<div class="mb-[4px] flex items-center justify-between">
|
||||
<div>{{ item.num }}</div>
|
||||
<div class="flex items-center gap-[4px] leading-[22px]">
|
||||
<MsIcon
|
||||
:type="reviewResultMap[item.status]?.icon"
|
||||
:style="{ color: reviewResultMap[item.status]?.color }"
|
||||
/>
|
||||
{{ t(reviewResultMap[item.status]?.label) }}
|
||||
</div>
|
||||
</div>
|
||||
<a-tooltip :content="item.name">
|
||||
<div class="one-line-text">{{ item.name }}</div>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<a-tooltip :content="item.name">
|
||||
<div class="one-line-text">{{ item.name }}</div>
|
||||
</a-tooltip>
|
||||
<MsEmpty v-if="caseList.length === 0" />
|
||||
</div>
|
||||
</div>
|
||||
<MsPagination :total="total" :page-size="pageSize" :current="pageCurrent" size="mini" simple />
|
||||
<MsPagination
|
||||
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 class="relative flex w-[calc(100%-356px)] flex-col">
|
||||
<div class="pl-[16px] pt-[16px]">
|
||||
<div class="rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[16px]">
|
||||
<div class="mb-[12px] flex items-center justify-between">
|
||||
<a-tooltip :content="activeCase.module">
|
||||
<div class="one-line-text cursor-pointer font-medium text-[rgb(var(--primary-5))]">
|
||||
【{{ activeCase.id }}】{{ activeCase.name }}
|
||||
<a-tooltip :content="`【${caseDetail.num}】${caseDetail.name}`">
|
||||
<div
|
||||
class="one-line-text cursor-pointer font-medium text-[rgb(var(--primary-5))]"
|
||||
@click="goCaseDetail"
|
||||
>
|
||||
【{{ caseDetail.num }}】{{ caseDetail.name }}
|
||||
</div>
|
||||
</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') }}
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<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)]">
|
||||
{{ activeCase.module }}
|
||||
{{ caseDetail.moduleName || t('common.root') }}
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<div class="case-detail-label">
|
||||
{{ t('caseManagement.caseReview.caseLevel') }}
|
||||
</div>
|
||||
<div class="case-detail-value">
|
||||
<caseLevel :case-level="activeCase.level" />
|
||||
<caseLevel :case-level="caseDetailLevel" />
|
||||
</div>
|
||||
<div class="case-detail-label">
|
||||
{{ t('caseManagement.caseReview.caseVersion') }}
|
||||
</div>
|
||||
<div class="case-detail-value">
|
||||
<MsIcon type="icon-icon_version" size="13" class="mr-[4px]" />
|
||||
{{ activeCase.version }}
|
||||
{{ caseDetail.versionName }}
|
||||
</div>
|
||||
<div class="case-detail-label">
|
||||
{{ t('caseManagement.caseReview.reviewResult') }}
|
||||
</div>
|
||||
<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
|
||||
:type="resultMap[activeCase.result].icon"
|
||||
:type="reviewResultMap[activeCaseReviewStatus as ReviewResult].icon"
|
||||
: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>
|
||||
|
@ -128,41 +157,49 @@
|
|||
<div v-else-if="showTab === 'detail'" class="h-full">
|
||||
<MsSplitBox :size="0.8" direction="vertical" min="0" :max="0.99">
|
||||
<template #first>
|
||||
<caseTabDetail :form="detailForm" :allow-edit="false" />
|
||||
<caseTabDetail :form="caseDetail" :allow-edit="false" />
|
||||
</template>
|
||||
<template #second>
|
||||
<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') }}
|
||||
</div>
|
||||
<div class="review-history-list">
|
||||
<div v-for="item of reviewHistoryList" :key="item.id" class="mb-[16px]">
|
||||
<div class="flex items-center">
|
||||
<a-avatar>A</a-avatar>
|
||||
<div class="ml-[8px] flex items-center">
|
||||
<div class="font-medium text-[var(--color-text-1)]">{{ item.reviewer }}</div>
|
||||
<a-divider direction="vertical" margin="8px"></a-divider>
|
||||
<div v-if="item.result === 1" class="flex items-center">
|
||||
<MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--success-6))]" />
|
||||
{{ t('caseManagement.caseReview.pass') }}
|
||||
</div>
|
||||
<div v-else-if="item.result === 2" class="flex items-center">
|
||||
<MsIcon type="icon-icon_close_filled" class="mr-[4px] text-[rgb(var(--danger-6))]" />
|
||||
{{ t('caseManagement.caseReview.fail') }}
|
||||
</div>
|
||||
<div v-else-if="item.result === 3" class="flex items-center">
|
||||
<MsIcon type="icon-icon_warning_filled" class="mr-[4px] text-[rgb(var(--warning-6))]" />
|
||||
{{ t('caseManagement.caseReview.suggestion') }}
|
||||
</div>
|
||||
<div v-else-if="item.result === 4" class="flex items-center">
|
||||
<MsIcon type="icon-icon_resubmit_filled" class="mr-[4px] text-[rgb(var(--warning-6))]" />
|
||||
{{ t('caseManagement.caseReview.reReview') }}
|
||||
<a-spin :loading="reviewHistoryListLoading" class="h-full w-full">
|
||||
<div v-for="item of reviewHistoryList" :key="item.id" class="mb-[16px]">
|
||||
<div class="flex items-center">
|
||||
<MSAvatar :avatar="item.userLogo" />
|
||||
<div class="ml-[8px] flex items-center">
|
||||
<div class="font-medium text-[var(--color-text-1)]">{{ item.userName }}</div>
|
||||
<a-divider direction="vertical" margin="8px"></a-divider>
|
||||
<div v-if="item.status === 'PASS'" class="flex items-center">
|
||||
<MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--success-6))]" />
|
||||
{{ t('caseManagement.caseReview.pass') }}
|
||||
</div>
|
||||
<div v-else-if="item.status === 'UN_PASS'" class="flex items-center">
|
||||
<MsIcon type="icon-icon_close_filled" class="mr-[4px] text-[rgb(var(--danger-6))]" />
|
||||
{{ t('caseManagement.caseReview.fail') }}
|
||||
</div>
|
||||
<div v-else-if="item.status === 'UNDER_REVIEWED'" class="flex items-center">
|
||||
<MsIcon type="icon-icon_warning_filled" class="mr-[4px] text-[rgb(var(--warning-6))]" />
|
||||
{{ t('caseManagement.caseReview.suggestion') }}
|
||||
</div>
|
||||
<div v-else-if="item.status === 'RE_REVIEWED'" class="flex items-center">
|
||||
<MsIcon
|
||||
type="icon-icon_resubmit_filled"
|
||||
class="mr-[4px] text-[rgb(var(--warning-6))]"
|
||||
/>
|
||||
{{ t('caseManagement.caseReview.reReview') }}
|
||||
</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 class="ml-[48px] text-[var(--color-text-2)]">{{ item.reason }}</div>
|
||||
<div class="ml-[48px] mt-[8px] text-[var(--color-text-4)]">{{ item.time }}</div>
|
||||
</div>
|
||||
<MsEmpty v-if="reviewHistoryList.length === 0" />
|
||||
</a-spin>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -182,7 +219,7 @@
|
|||
</div>
|
||||
<caseTabDemand
|
||||
ref="caseDemandRef"
|
||||
:fun-params="{ caseId: route.query.id as string, keyword: demandKeyword }"
|
||||
:fun-params="{ caseId: route.query.caseId as string, keyword: demandKeyword }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -207,19 +244,19 @@
|
|||
<a-form ref="dialogFormRef" :model="caseResultForm" layout="vertical">
|
||||
<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 value="pass">
|
||||
<a-radio value="PASS">
|
||||
<div class="inline-flex items-center">
|
||||
<MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--success-6))]" />
|
||||
{{ t('caseManagement.caseReview.pass') }}
|
||||
</div>
|
||||
</a-radio>
|
||||
<a-radio value="fail">
|
||||
<a-radio value="UN_PASS">
|
||||
<div class="inline-flex items-center">
|
||||
<MsIcon type="icon-icon_close_filled" class="mr-[4px] text-[rgb(var(--danger-6))]" />
|
||||
{{ t('caseManagement.caseReview.fail') }}
|
||||
</div>
|
||||
</a-radio>
|
||||
<a-radio value="suggestion">
|
||||
<a-radio value="UNDER_REVIEWED">
|
||||
<div class="inline-flex items-center">
|
||||
<MsIcon type="icon-icon_warning_filled" class="mr-[4px] text-[rgb(var(--warning-6))]" />
|
||||
{{ t('caseManagement.caseReview.suggestion') }}
|
||||
|
@ -237,20 +274,17 @@
|
|||
field="reason"
|
||||
:label="t('caseManagement.caseReview.reason')"
|
||||
:rules="
|
||||
caseResultForm.result === 'fail'
|
||||
caseResultForm.result === 'UN_PASS'
|
||||
? [{ required: true, message: t('caseManagement.caseReview.reasonRequired') }]
|
||||
: []
|
||||
"
|
||||
asterisk-position="end"
|
||||
class="mb-0"
|
||||
>
|
||||
<a-input
|
||||
v-model:model-value="caseResultForm.reason"
|
||||
:placeholder="t('caseManagement.caseReview.reasonPlaceholder')"
|
||||
/>
|
||||
<MsRichText v-model:modelValue="caseResultForm.reason" class="w-full" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-button type="primary" class="mt-[16px]">
|
||||
<a-button type="primary" class="mt-[16px]" :loading="submitReviewLoading" @click="submitReview">
|
||||
{{ t('caseManagement.caseReview.submitReview') }}
|
||||
</a-button>
|
||||
</div>
|
||||
|
@ -258,73 +292,136 @@
|
|||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* @description 功能测试-用例评审-用例详情
|
||||
*/
|
||||
import { useRoute } from 'vue-router';
|
||||
import { FormInstance } from '@arco-design/web-vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { FormInstance, Message } from '@arco-design/web-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import MSAvatar from '@/components/pure/ms-avatar/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 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 caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
|
||||
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 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 useAppStore from '@/store/modules/app';
|
||||
|
||||
import { ReviewCaseItem, ReviewHistoryItem, ReviewItem, ReviewResult } from '@/models/caseManagement/caseReview';
|
||||
import type { DetailCase } from '@/models/caseManagement/featureCase';
|
||||
import { CaseManagementRouteEnum } from '@/enums/routeEnum';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const reviewName = ref('打算肯定还是觉得还是觉得还是计划的');
|
||||
const caseDetail = ref({
|
||||
demandCount: 999,
|
||||
});
|
||||
const onlyMine = ref(false);
|
||||
const keyword = ref('');
|
||||
const reviewDetail = ref<ReviewItem>({ ...reviewDefaultDetail });
|
||||
const loading = ref(false);
|
||||
|
||||
// 初始化评审详情
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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 typeOptions = ref([
|
||||
{ label: '全部', value: '' },
|
||||
{ label: resultMap[0].label, value: 'unReview' },
|
||||
{ label: resultMap[1].label, value: 'reviewPass' },
|
||||
{ label: resultMap[2].label, value: 'fail' },
|
||||
{ label: resultMap[3].label, value: 'reReview' },
|
||||
{ label: t('common.all'), value: '' },
|
||||
{ label: t(reviewResultMap.UN_REVIEWED.label), value: 'UN_REVIEWED' },
|
||||
{ label: t(reviewResultMap.PASS.label), value: 'PASS' },
|
||||
{ label: t(reviewResultMap.UN_PASS.label), value: 'UN_PASS' },
|
||||
{ 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: '',
|
||||
projectId: '',
|
||||
templateId: '',
|
||||
|
@ -341,60 +438,57 @@
|
|||
tags: [],
|
||||
customFields: [], // 自定义字段集合
|
||||
relateFileMetaIds: [], // 关联文件ID集合
|
||||
reviewStatus: 'UN_REVIEWED',
|
||||
functionalPriority: '',
|
||||
};
|
||||
const detailForm = ref<DetailCase>({ ...initDetail });
|
||||
|
||||
const caseList = ref([
|
||||
{
|
||||
id: 'g4ggtrgrtg',
|
||||
name: '打算肯定还是觉得还是觉得还是计划的',
|
||||
module: '模块名称模块名称模块名称模块名称模块名称模块名称模块名称模块名称模块名称',
|
||||
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',
|
||||
const caseDetail = ref<DetailCase>({ ...defaultCaseDetail });
|
||||
const descriptions = ref<Description[]>([]);
|
||||
const caseDetailLevel = computed<CaseLevel>(() => {
|
||||
if (caseDetail.value.functionalPriority) {
|
||||
return (Number(JSON.parse(caseDetail.value.functionalPriority).match(/\d+/g)[0]) as CaseLevel) || 0; // 匹配出用例等级的数字
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
function changeActiveCase(item: any) {
|
||||
if (activeCase.value.id !== item.id) {
|
||||
activeCase.value = item;
|
||||
function changeActiveCase(item: ReviewCaseItem) {
|
||||
if (activeCaseId.value !== item.caseId) {
|
||||
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 tabList = ref([
|
||||
{
|
||||
|
@ -411,67 +505,36 @@
|
|||
},
|
||||
]);
|
||||
|
||||
const descriptions = ref([
|
||||
{
|
||||
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 reviewHistoryListLoading = ref(false);
|
||||
const reviewHistoryList = ref<ReviewHistoryItem[]>([]);
|
||||
|
||||
const reviewHistoryList = ref([
|
||||
{
|
||||
id: 1,
|
||||
reviewer: '张三',
|
||||
avatar: '',
|
||||
result: 1,
|
||||
reason: '',
|
||||
time: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
reviewer: '李四',
|
||||
avatar: '',
|
||||
result: 2,
|
||||
reason: '不通过',
|
||||
time: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
reviewer: '王五',
|
||||
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'),
|
||||
},
|
||||
]);
|
||||
// 加载评审历史列表
|
||||
async function initReviewHistoryList() {
|
||||
try {
|
||||
reviewHistoryListLoading.value = true;
|
||||
const res = await getCaseReviewHistoryList(route.query.id as string, activeCaseId.value);
|
||||
reviewHistoryList.value = res;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
reviewHistoryListLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => activeCaseId.value,
|
||||
() => {
|
||||
loadCaseDetail();
|
||||
if (showTab.value === 'detail') {
|
||||
initReviewHistoryList();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const autoNext = ref(false);
|
||||
const caseResultForm = ref({
|
||||
result: 'pass',
|
||||
result: 'PASS' as ReviewResult,
|
||||
reason: '',
|
||||
});
|
||||
const dialogFormRef = ref<FormInstance>();
|
||||
|
@ -481,6 +544,110 @@
|
|||
function searchDemand() {
|
||||
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>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
@ -493,8 +660,10 @@
|
|||
background: var(--color-text-n9);
|
||||
.case-item {
|
||||
@apply cursor-pointer;
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
margin-bottom: 8px;
|
||||
padding: 16px;
|
||||
border-radius: var(--border-radius-small);
|
||||
background-color: white;
|
||||
|
|
|
@ -5,21 +5,18 @@
|
|||
v-model:currentSelectCase="currentSelectCase"
|
||||
:ok-button-disabled="associateForm.reviewers.length === 0"
|
||||
:get-modules-func="getCaseModuleTree"
|
||||
:modules-params="modulesTreeParams"
|
||||
:get-table-func="getCaseList"
|
||||
:modules-count="modulesCount"
|
||||
:confirm-loading="confirmLoading"
|
||||
:associated-ids="associatedIds"
|
||||
@close="emit('close')"
|
||||
@save="saveHandler"
|
||||
@init="getModuleCount"
|
||||
>
|
||||
<template #footerLeft>
|
||||
<a-form ref="associateFormRef" :model="associateForm">
|
||||
<a-form-item
|
||||
field="reviewers"
|
||||
:rules="[{ required: true, message: t('caseManagement.caseReview.reviewerRequired') }]"
|
||||
class="mb-0"
|
||||
class="review-item mb-0"
|
||||
>
|
||||
<template #label>
|
||||
<div class="inline-flex items-center">
|
||||
|
@ -52,7 +49,7 @@
|
|||
allow-search
|
||||
allow-clear
|
||||
multiple
|
||||
class="w-[300px]"
|
||||
class="w-[290px]"
|
||||
:loading="reviewerLoading"
|
||||
>
|
||||
<template #empty>
|
||||
|
@ -73,19 +70,19 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { FormInstance, SelectOptionData } from '@arco-design/web-vue';
|
||||
|
||||
import MsCaseAssociate from '@/components/business/ms-case-associate/index.vue';
|
||||
import MsSelect from '@/components/business/ms-select';
|
||||
|
||||
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 useLocale from '@/locale/useLocale';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import type { CaseModuleQueryParams } from '@/models/caseManagement/featureCase';
|
||||
import type { TableQueryParams } from '@/models/common';
|
||||
import { BaseAssociateCaseRequest } from '@/models/caseManagement/caseReview';
|
||||
import { ProjectManagementRouteEnum } from '@/enums/routeEnum';
|
||||
|
||||
const props = defineProps<{
|
||||
|
@ -95,7 +92,7 @@
|
|||
const emit = defineEmits<{
|
||||
(e: 'update:visible', val: boolean): void;
|
||||
(e: 'update:project', val: string): void;
|
||||
(e: 'success', val: string[]): void;
|
||||
(e: 'success', val: BaseAssociateCaseRequest & { reviewers: string[] }): void;
|
||||
(e: 'close'): void;
|
||||
}>();
|
||||
const router = useRouter();
|
||||
|
@ -103,42 +100,11 @@
|
|||
const { currentLocale } = useLocale();
|
||||
const { t } = useI18n();
|
||||
|
||||
const innerVisible = ref(false);
|
||||
|
||||
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 innerVisible = useVModel(props, 'visible', emit);
|
||||
const innerProject = useVModel(props, 'project', emit);
|
||||
|
||||
const associateForm = ref({
|
||||
reviewers: [],
|
||||
reviewers: [] as string[],
|
||||
});
|
||||
const associateFormRef = ref<FormInstance>();
|
||||
|
||||
|
@ -167,24 +133,44 @@
|
|||
}
|
||||
|
||||
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 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(() => {
|
||||
initReviewers();
|
||||
});
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
// 抽屉打开才加载数据
|
||||
initReviewers();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
<style lang="less" scoped>
|
||||
:deep(.review-item) {
|
||||
.arco-form-item-label-col {
|
||||
flex: none;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,33 +1,28 @@
|
|||
<template>
|
||||
<div class="px-[24px] py-[16px]">
|
||||
<div class="mb-[16px] flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<a-button type="primary" class="mr-[12px]" @click="associateDrawerVisible = true">
|
||||
{{ t('ms.case.associate.title') }}
|
||||
</a-button>
|
||||
<a-button type="outline" @click="createCase">{{ t('caseManagement.caseReview.createCase') }}</a-button>
|
||||
</div>
|
||||
<div class="flex w-[70%] items-center justify-end gap-[8px]">
|
||||
<a-input-search
|
||||
v-model="keyword"
|
||||
:placeholder="t('caseManagement.caseReview.searchPlaceholder')"
|
||||
allow-clear
|
||||
class="w-[200px]"
|
||||
@press-enter="searchReview"
|
||||
@search="searchReview"
|
||||
/>
|
||||
<a-button type="outline" class="arco-btn-outline--secondary px-[8px]">
|
||||
<MsIcon type="icon-icon-filter" class="mr-[4px] text-[var(--color-text-4)]" />
|
||||
<div class="text-[var(--color-text-4)]">{{ t('common.filter') }}</div>
|
||||
</a-button>
|
||||
<a-radio-group v-model:model-value="showType" type="button" class="case-show-type">
|
||||
<a-radio value="list" class="show-type-icon p-[2px]"><MsIcon type="icon-icon_view-list_outlined" /></a-radio>
|
||||
<a-radio value="mind" class="show-type-icon p-[2px]"><MsIcon type="icon-icon_mindnote_outlined" /></a-radio>
|
||||
</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 class="mb-[16px] flex flex-wrap items-center justify-end">
|
||||
<MsAdvanceFilter
|
||||
v-model:keyword="keyword"
|
||||
:filter-config-list="filterConfigList"
|
||||
:row-count="filterRowCount"
|
||||
:search-placeholder="t('caseManagement.caseReview.searchPlaceholder')"
|
||||
@keyword-search="() => searchCase()"
|
||||
@adv-search="searchCase"
|
||||
@reset="searchCase"
|
||||
>
|
||||
<template #right>
|
||||
<div class="flex items-center">
|
||||
<a-radio-group v-model:model-value="showType" type="button" class="case-show-type">
|
||||
<a-radio value="list" class="show-type-icon p-[2px]">
|
||||
<MsIcon type="icon-icon_view-list_outlined" />
|
||||
</a-radio>
|
||||
<a-radio value="mind" class="show-type-icon p-[2px]">
|
||||
<MsIcon type="icon-icon_mindnote_outlined" />
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
</MsAdvanceFilter>
|
||||
</div>
|
||||
<ms-base-table
|
||||
v-bind="propsRes"
|
||||
|
@ -51,20 +46,25 @@
|
|||
</template>
|
||||
<template #name="{ record }">
|
||||
<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>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</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]">
|
||||
<MsIcon
|
||||
:type="resultMap[record.result as ResultMap].icon"
|
||||
:type="reviewResultMap[record.status as ReviewResult].icon"
|
||||
: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>
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
|
@ -76,7 +76,9 @@
|
|||
:title="t('caseManagement.caseReview.disassociateTip')"
|
||||
:sub-title-tip="t('caseManagement.caseReview.disassociateTipContent')"
|
||||
:ok-text="t('common.confirm')"
|
||||
:loading="disassociateLoading"
|
||||
type="error"
|
||||
@confirm="(val, done) => handleDisassociateReviewCase(record, done)"
|
||||
>
|
||||
<MsButton type="text" class="!mr-0">
|
||||
{{ t('caseManagement.caseReview.disassociate') }}
|
||||
|
@ -98,6 +100,7 @@
|
|||
class="p-[4px]"
|
||||
title-align="start"
|
||||
body-class="p-0"
|
||||
:width="['review', 'reReview'].includes(dialogShowType) ? 680 : 480"
|
||||
:mask-closable="false"
|
||||
@close="handleDialogCancel"
|
||||
>
|
||||
|
@ -129,13 +132,13 @@
|
|||
class="mb-[16px]"
|
||||
>
|
||||
<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">
|
||||
<MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--success-6))]" />
|
||||
{{ t('caseManagement.caseReview.pass') }}
|
||||
</div>
|
||||
</a-radio>
|
||||
<a-radio value="fail">
|
||||
<a-radio value="UN_PASS">
|
||||
<div class="inline-flex items-center">
|
||||
<MsIcon type="icon-icon_close_filled" class="mr-[4px] text-[rgb(var(--danger-6))]" />
|
||||
{{ t('caseManagement.caseReview.fail') }}
|
||||
|
@ -155,10 +158,11 @@
|
|||
asterisk-position="end"
|
||||
class="mb-0"
|
||||
>
|
||||
<a-input
|
||||
<!-- <a-input
|
||||
v-model:model-value="dialogForm.reason"
|
||||
:placeholder="t('caseManagement.caseReview.reasonPlaceholder')"
|
||||
/>
|
||||
/> -->
|
||||
<MsRichText v-model:modelValue="dialogForm.reason" class="w-full" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="dialogShowType === 'changeReviewer'"
|
||||
|
@ -171,6 +175,7 @@
|
|||
<MsSelect
|
||||
v-model:modelValue="dialogForm.reviewer"
|
||||
mode="static"
|
||||
:loading="reviewerLoading"
|
||||
:placeholder="t('caseManagement.caseReview.reviewerPlaceholder')"
|
||||
:options="reviewersOptions"
|
||||
:search-keys="['label']"
|
||||
|
@ -195,94 +200,104 @@
|
|||
/>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<a-button type="secondary" @click="handleDialogCancel">{{ t('common.cancel') }}</a-button>
|
||||
<a-button v-if="dialogShowType === 'review'" type="primary" class="ml-[12px]" @click="commitResult">
|
||||
<a-button type="secondary" :disabled="dialogLoading" @click="handleDialogCancel">
|
||||
{{ t('common.cancel') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="dialogShowType === 'review'"
|
||||
type="primary"
|
||||
class="ml-[12px]"
|
||||
:loading="dialogLoading"
|
||||
@click="commitResult"
|
||||
>
|
||||
{{ t('caseManagement.caseReview.commitResult') }}
|
||||
</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') }}
|
||||
</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') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</a-modal>
|
||||
<AssociateDrawer
|
||||
v-model:visible="associateDrawerVisible"
|
||||
v-model:project="associateDrawerProject"
|
||||
@success="writeAssociateCases"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
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 MsIcon from '@/components/pure/ms-icon-font/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 type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type';
|
||||
import useTable from '@/components/pure/ms-table/useTable';
|
||||
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 useModal from '@/hooks/useModal';
|
||||
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 { TableKeyEnum } from '@/enums/tableEnum';
|
||||
|
||||
const props = defineProps<{
|
||||
activeFolder: string | number;
|
||||
onlyMine: boolean;
|
||||
reviewPassRule: ReviewPassRule; // 评审规则
|
||||
offspringIds: string[]; // 当前选中节点的所有子节点id
|
||||
moduleTree: ModuleTreeNode[];
|
||||
}>();
|
||||
const emit = defineEmits(['init', 'refresh']);
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const appStore = useAppStore();
|
||||
const userStore = useUserStore();
|
||||
const { t } = useI18n();
|
||||
const { openModal } = useModal();
|
||||
|
||||
const keyword = ref('');
|
||||
const showType = ref<'list' | 'mind'>('list');
|
||||
|
||||
type ResultMap = 0 | 1 | 2 | 3 | 4;
|
||||
const resultMap = {
|
||||
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 filterRowCount = ref(0);
|
||||
const filterConfigList = ref<FilterFormItem[]>([]);
|
||||
const tableParams = ref<Record<string, any>>({});
|
||||
const columns: MsTableColumn = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
dataIndex: 'num',
|
||||
sortIndex: 1,
|
||||
showTooltip: true,
|
||||
width: 100,
|
||||
|
@ -298,8 +313,8 @@
|
|||
},
|
||||
{
|
||||
title: 'caseManagement.caseReview.reviewer',
|
||||
dataIndex: 'reviewer',
|
||||
showTooltip: true,
|
||||
dataIndex: 'reviewNames',
|
||||
slotName: 'reviewNames',
|
||||
sortable: {
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
},
|
||||
|
@ -307,14 +322,14 @@
|
|||
},
|
||||
{
|
||||
title: 'caseManagement.caseReview.reviewResult',
|
||||
dataIndex: 'result',
|
||||
slotName: 'result',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
titleSlotName: 'resultColumn',
|
||||
width: 110,
|
||||
},
|
||||
{
|
||||
title: 'caseManagement.caseReview.version',
|
||||
dataIndex: 'version',
|
||||
dataIndex: 'versionName',
|
||||
width: 90,
|
||||
},
|
||||
{
|
||||
|
@ -332,14 +347,17 @@
|
|||
];
|
||||
const tableStore = useTableStore();
|
||||
tableStore.initColumn(TableKeyEnum.CASE_MANAGEMENT_REVIEW_CASE, columns, 'drawer');
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(getReviewList, {
|
||||
scroll: { x: '100%' },
|
||||
tableKey: TableKeyEnum.CASE_MANAGEMENT_REVIEW_CASE,
|
||||
showSetting: true,
|
||||
selectable: true,
|
||||
showSelectAll: true,
|
||||
draggable: { type: 'handle', width: 32 },
|
||||
});
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, getTableQueryParams } = useTable(
|
||||
getReviewDetailCasePage,
|
||||
{
|
||||
scroll: { x: '100%' },
|
||||
tableKey: TableKeyEnum.CASE_MANAGEMENT_REVIEW_CASE,
|
||||
showSetting: true,
|
||||
selectable: true,
|
||||
showSelectAll: true,
|
||||
draggable: { type: 'handle', width: 32 },
|
||||
}
|
||||
);
|
||||
const batchActions = {
|
||||
baseAction: [
|
||||
{
|
||||
|
@ -361,23 +379,54 @@
|
|||
],
|
||||
};
|
||||
|
||||
function searchReview() {
|
||||
setLoadListParams({
|
||||
function searchCase(filter?: FilterResult) {
|
||||
tableParams.value = {
|
||||
projectId: appStore.currentProjectId,
|
||||
reviewId: route.query.id,
|
||||
moduleIds: props.activeFolder === 'all' ? [] : [props.activeFolder, ...props.offspringIds],
|
||||
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();
|
||||
emit('init', {
|
||||
...tableParams.value,
|
||||
moduleIds: [],
|
||||
});
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
loadList();
|
||||
searchCase();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.onlyMine,
|
||||
() => {
|
||||
searchCase();
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.activeFolder,
|
||||
() => {
|
||||
searchCase();
|
||||
}
|
||||
);
|
||||
|
||||
const tableSelected = ref<(string | number)[]>([]);
|
||||
const batchParams = ref<BatchActionQueryParams>({
|
||||
selectedIds: [],
|
||||
const batchParams = ref<BatchApiParams>({
|
||||
selectIds: [],
|
||||
selectAll: false,
|
||||
excludeIds: [],
|
||||
currentSelectCount: 0,
|
||||
excludeIds: [] as string[],
|
||||
condition: {},
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -388,20 +437,16 @@
|
|||
}
|
||||
|
||||
const dialogVisible = ref<boolean>(false);
|
||||
const activeRecord = ref({
|
||||
id: '',
|
||||
name: '',
|
||||
status: 0,
|
||||
});
|
||||
const defaultDialogForm = {
|
||||
result: 'pass',
|
||||
result: 'PASS',
|
||||
reason: '',
|
||||
reviewer: [],
|
||||
reviewer: [] as string[],
|
||||
isAppend: false,
|
||||
};
|
||||
const dialogForm = ref({ ...defaultDialogForm });
|
||||
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(() => {
|
||||
switch (dialogShowType.value) {
|
||||
case 'review':
|
||||
|
@ -414,20 +459,6 @@
|
|||
return '';
|
||||
}
|
||||
});
|
||||
const reviewersOptions = ref([
|
||||
{
|
||||
label: '张三',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: '李四',
|
||||
value: '2',
|
||||
},
|
||||
{
|
||||
label: '王五',
|
||||
value: '3',
|
||||
},
|
||||
]);
|
||||
|
||||
function handleDialogCancel() {
|
||||
dialogVisible.value = false;
|
||||
|
@ -435,8 +466,30 @@
|
|||
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 关闭弹窗
|
||||
*/
|
||||
async function handleDeleteConfirm(done: (closed: boolean) => void) {
|
||||
|
@ -456,7 +509,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
function handleArchive(record: any) {
|
||||
function handleArchive(record: ReviewItem) {
|
||||
openModal({
|
||||
type: 'warning',
|
||||
title: t('caseManagement.caseReview.archivedTitle', { name: record.name }),
|
||||
|
@ -482,16 +535,8 @@
|
|||
});
|
||||
}
|
||||
|
||||
const selectedModuleKeys = ref<(string | number)[]>([]);
|
||||
|
||||
/**
|
||||
* 处理文件夹树节点选中事件
|
||||
*/
|
||||
function folderNodeSelect(keys: (string | number)[]) {
|
||||
selectedModuleKeys.value = keys;
|
||||
}
|
||||
|
||||
function disassociate() {
|
||||
// 批量解除关联用例拦截
|
||||
function batchDisassociate() {
|
||||
openModal({
|
||||
type: 'warning',
|
||||
title: t('caseManagement.caseReview.disassociateConfirmTitle', { count: tableSelected.value.length }),
|
||||
|
@ -500,82 +545,157 @@
|
|||
cancelText: t('common.cancel'),
|
||||
onBeforeOk: async () => {
|
||||
try {
|
||||
// await resetUserPassword({
|
||||
// selectIds,
|
||||
// selectAll: !!params?.selectAll,
|
||||
// excludeIds: params?.excludeIds || [],
|
||||
// condition: { keyword: keyword.value },
|
||||
// });
|
||||
dialogLoading.value = true;
|
||||
await batchDisassociateReviewCase({
|
||||
reviewId: route.query.id as string,
|
||||
userId: props.onlyMine ? userStore.id || '' : '',
|
||||
selectIds: batchParams.value.selectIds,
|
||||
selectAll: batchParams.value.selectAll,
|
||||
excludeIds: batchParams.value.excludeIds,
|
||||
condition: batchParams.value.condition,
|
||||
});
|
||||
Message.success(t('common.updateSuccess'));
|
||||
resetSelector();
|
||||
loadList();
|
||||
emit('refresh');
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
dialogLoading.value = false;
|
||||
}
|
||||
},
|
||||
hideCancel: false,
|
||||
});
|
||||
}
|
||||
|
||||
function reReview() {
|
||||
// 批量重新评审
|
||||
async function reReview() {
|
||||
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'));
|
||||
dialogVisible.value = false;
|
||||
resetSelector();
|
||||
emit('refresh');
|
||||
loadList();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
dialogLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 批量更换评审人
|
||||
function changeReviewer() {
|
||||
dialogFormRef.value?.validate(async (errors) => {
|
||||
if (!errors) {
|
||||
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'));
|
||||
dialogVisible.value = false;
|
||||
resetSelector();
|
||||
loadList();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
dialogLoading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 提交评审结果
|
||||
function commitResult() {
|
||||
dialogFormRef.value?.validate(async (errors) => {
|
||||
if (!errors) {
|
||||
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'));
|
||||
dialogVisible.value = false;
|
||||
resetSelector();
|
||||
emit('refresh');
|
||||
loadList();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
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 批量操作事件对象
|
||||
*/
|
||||
function handleTableBatch(event: BatchActionParams, params: BatchActionQueryParams) {
|
||||
tableSelected.value = params?.selectedIds || [];
|
||||
batchParams.value = params;
|
||||
batchParams.value = { ...params, selectIds: params?.selectedIds || [], condition: {} };
|
||||
switch (event.eventTag) {
|
||||
case 'review':
|
||||
dialogVisible.value = true;
|
||||
dialogShowType.value = 'review';
|
||||
break;
|
||||
case 'changeReviewer':
|
||||
initReviewers();
|
||||
dialogVisible.value = true;
|
||||
dialogShowType.value = 'changeReviewer';
|
||||
break;
|
||||
case 'disassociate':
|
||||
disassociate();
|
||||
batchDisassociate();
|
||||
break;
|
||||
case 'reReview':
|
||||
dialogVisible.value = true;
|
||||
|
@ -586,20 +706,20 @@
|
|||
}
|
||||
}
|
||||
|
||||
function openDetail(id: string) {
|
||||
// 去用例评审页面
|
||||
function review(record: ReviewCaseItem) {
|
||||
router.push({
|
||||
name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW_DETAIL_CASE_DETAIL,
|
||||
query: {
|
||||
...route.query,
|
||||
caseId: id,
|
||||
caseId: record.caseId,
|
||||
},
|
||||
state: {
|
||||
params: JSON.stringify(getTableQueryParams()),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function review(record: any) {
|
||||
console.log('review');
|
||||
}
|
||||
|
||||
function createCase() {
|
||||
router.push({
|
||||
name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE_DETAIL,
|
||||
|
@ -609,12 +729,130 @@
|
|||
});
|
||||
}
|
||||
|
||||
const associateDrawerVisible = ref(false);
|
||||
const associateDrawerProject = ref('');
|
||||
onBeforeMount(async () => {
|
||||
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[]) {
|
||||
console.log('writeAssociateCases', ids);
|
||||
}
|
||||
defineExpose({
|
||||
searchCase,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<div :class="getFolderClass('all')" @click="setActiveFolder('all')">
|
||||
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
|
||||
<div class="folder-name">{{ t('caseManagement.caseReview.allCases') }}</div>
|
||||
<div class="folder-count">({{ allFileCount }})</div>
|
||||
<div class="folder-count">({{ allCount }})</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider class="my-[8px]" />
|
||||
|
@ -47,12 +47,14 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
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 MsTree from '@/components/business/ms-tree/index.vue';
|
||||
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 useAppStore from '@/store/modules/app';
|
||||
import { mapTree } from '@/utils';
|
||||
|
@ -63,9 +65,11 @@
|
|||
modulesCount?: Record<string, number>; // 模块数量统计对象
|
||||
showType?: string; // 显示类型
|
||||
isExpandAll?: boolean; // 是否展开所有节点
|
||||
selectedKeys: string[]; // 选中的节点 key
|
||||
}>();
|
||||
const emit = defineEmits(['init', 'folderNodeSelect']);
|
||||
|
||||
const route = useRoute();
|
||||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
|
@ -76,7 +80,7 @@
|
|||
});
|
||||
|
||||
const activeFolder = ref<string>('all');
|
||||
const allFileCount = ref(0);
|
||||
const allCount = ref(0);
|
||||
const isExpandAll = ref(props.isExpandAll);
|
||||
|
||||
watch(
|
||||
|
@ -99,31 +103,22 @@
|
|||
const folderTree = ref<ModuleTreeNode[]>([]);
|
||||
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 {
|
||||
loading.value = true;
|
||||
const res = await getModules(appStore.currentProjectId);
|
||||
folderTree.value = res;
|
||||
if (isSetDefaultKey) {
|
||||
selectedKeys.value = [folderTree.value[0].id];
|
||||
const offspringIds: string[] = [];
|
||||
mapTree(folderTree.value[0].children || [], (e) => {
|
||||
offspringIds.push(e.id);
|
||||
return e;
|
||||
});
|
||||
|
||||
emit('folderNodeSelect', selectedKeys.value, offspringIds);
|
||||
}
|
||||
emit(
|
||||
'init',
|
||||
folderTree.value.map((e) => e.name)
|
||||
);
|
||||
const res = await getReviewDetailModuleTree(appStore.currentProjectId, route.query.id as string);
|
||||
folderTree.value = mapTree<ModuleTreeNode>(res, (node) => {
|
||||
return {
|
||||
...node,
|
||||
count: props.modulesCount?.[node.id] || 0,
|
||||
};
|
||||
});
|
||||
emit('init', folderTree.value);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
|
@ -141,7 +136,7 @@
|
|||
offspringIds.push(e.id);
|
||||
return e;
|
||||
});
|
||||
|
||||
activeFolder.value = node.id;
|
||||
emit('folderNodeSelect', _selectedKeys, offspringIds);
|
||||
}
|
||||
|
||||
|
@ -161,6 +156,7 @@
|
|||
count: obj?.[node.id] || 0,
|
||||
};
|
||||
});
|
||||
allCount.value = obj?.all || 0;
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -199,6 +199,7 @@
|
|||
hideMoreAction: e.id === 'root',
|
||||
draggable: e.id !== 'root' && !props.isModal,
|
||||
disabled: e.id === activeFolder.value && props.isModal,
|
||||
count: props.modulesCount?.[e.id] || 0, // 避免模块数量先初始化完成了,数量没更新
|
||||
};
|
||||
});
|
||||
if (isSetDefaultKey) {
|
||||
|
@ -265,8 +266,8 @@
|
|||
offspringIds.push(e.id);
|
||||
return e;
|
||||
});
|
||||
setActiveFolder(node.id);
|
||||
emit('folderNodeSelect', _selectedKeys, offspringIds);
|
||||
activeFolder.value = node.id;
|
||||
emit('folderNodeSelect', [node.id], offspringIds);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -342,6 +343,7 @@
|
|||
count: obj?.[node.id] || 0,
|
||||
};
|
||||
});
|
||||
allFileCount.value = obj?.all || 0;
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -86,7 +86,7 @@
|
|||
field: props.fieldConfig?.field || '',
|
||||
});
|
||||
const formRef = ref<FormInstance>();
|
||||
const loading = ref(false);
|
||||
const loading = ref(true);
|
||||
|
||||
watch(
|
||||
() => props.fieldConfig?.field,
|
||||
|
|
|
@ -6,8 +6,9 @@
|
|||
:filter-config-list="filterConfigList"
|
||||
:row-count="filterRowCount"
|
||||
:search-placeholder="t('caseManagement.caseReview.searchPlaceholder')"
|
||||
@keyword-search="searchReview"
|
||||
@keyword-search="() => searchReview()"
|
||||
@adv-search="searchReview"
|
||||
@reset="searchReview"
|
||||
>
|
||||
<template #left>
|
||||
<div class="flex items-center">
|
||||
|
@ -29,23 +30,23 @@
|
|||
<!-- <template #status-filter>
|
||||
<a-checkbox-group>
|
||||
<a-checkbox :value="0">
|
||||
<a-tag :color="statusMap[0].color" :class="statusMap[0].class">
|
||||
{{ t(statusMap[0].label) }}
|
||||
<a-tag :color="reviewStatusMap[0].color" :class="reviewStatusMap[0].class">
|
||||
{{ t(reviewStatusMap[0].label) }}
|
||||
</a-tag>
|
||||
</a-checkbox>
|
||||
<a-checkbox :value="1">
|
||||
<a-tag :color="statusMap[1].color" :class="statusMap[1].class">
|
||||
{{ t(statusMap[1].label) }}
|
||||
<a-tag :color="reviewStatusMap[1].color" :class="reviewStatusMap[1].class">
|
||||
{{ t(reviewStatusMap[1].label) }}
|
||||
</a-tag>
|
||||
</a-checkbox>
|
||||
<a-checkbox :value="2">
|
||||
<a-tag :color="statusMap[2].color" :class="statusMap[2].class">
|
||||
{{ t(statusMap[2].label) }}
|
||||
<a-tag :color="reviewStatusMap[2].color" :class="reviewStatusMap[2].class">
|
||||
{{ t(reviewStatusMap[2].label) }}
|
||||
</a-tag>
|
||||
</a-checkbox>
|
||||
<a-checkbox :value="3">
|
||||
<a-tag :color="statusMap[3].color" :class="statusMap[3].class">
|
||||
{{ t(statusMap[3].label) }}
|
||||
<a-tag :color="reviewStatusMap[3].color" :class="reviewStatusMap[3].class">
|
||||
{{ t(reviewStatusMap[3].label) }}
|
||||
</a-tag>
|
||||
</a-checkbox>
|
||||
</a-checkbox-group>
|
||||
|
@ -71,16 +72,28 @@
|
|||
<template #status="{ record }">
|
||||
<statusTag :status="record.status" />
|
||||
</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 }">
|
||||
<div class="mr-[8px] w-[100px]">
|
||||
<passRateLine :review-detail="record" height="5px" />
|
||||
</div>
|
||||
<div class="text-[var(--color-text-1)]">
|
||||
{{ `${(((record.passCount + record.failCount) / record.caseCount) * 100).toFixed(2)}%` }}
|
||||
{{ `${record.passRate}%` }}
|
||||
</div>
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<MsButton type="text" class="!mr-0">
|
||||
<MsButton type="text" class="!mr-0" @click="() => editReview(record)">
|
||||
{{ t('common.edit') }}
|
||||
</MsButton>
|
||||
<a-divider direction="vertical" :margin="8"></a-divider>
|
||||
|
@ -99,57 +112,7 @@
|
|||
</div>
|
||||
</template>
|
||||
</ms-base-table>
|
||||
<a-modal
|
||||
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>
|
||||
<deleteReviewModal v-model:visible="dialogVisible" :record="activeRecord" @success="loadList" />
|
||||
<a-modal
|
||||
v-model:visible="moveModalVisible"
|
||||
title-align="start"
|
||||
|
@ -188,7 +151,7 @@
|
|||
import dayjs from 'dayjs';
|
||||
|
||||
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 MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
||||
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 passRateLine from '../passRateLine.vue';
|
||||
import statusTag from '../statusTag.vue';
|
||||
import deleteReviewModal from './deleteReviewModal.vue';
|
||||
import ModuleTree from './moduleTree.vue';
|
||||
|
||||
import { getReviewList, getReviewUsers } from '@/api/modules/case-management/caseReview';
|
||||
import { reviewStatusMap } from '@/config/apiTest';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useModal from '@/hooks/useModal';
|
||||
import useTableStore from '@/hooks/useTableStore';
|
||||
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 { CaseManagementRouteEnum } from '@/enums/routeEnum';
|
||||
import { TableKeyEnum } from '@/enums/tableEnum';
|
||||
|
||||
const props = defineProps<{
|
||||
activeFolder: string | number;
|
||||
activeFolder: string;
|
||||
moduleTree: ModuleTreeNode[];
|
||||
showType: string;
|
||||
offspringIds: string[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'goCreate'): void;
|
||||
(e: 'init', params: ReviewListQueryParams): void;
|
||||
}>();
|
||||
|
||||
const userStore = useUserStore();
|
||||
const appStore = useAppStore();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
@ -225,35 +201,6 @@
|
|||
|
||||
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 filterConfigList = ref<FilterFormItem[]>([]);
|
||||
|
||||
|
@ -285,19 +232,19 @@
|
|||
mode: 'static',
|
||||
options: [
|
||||
{
|
||||
label: t(statusMap[0].label),
|
||||
label: t(reviewStatusMap.PREPARED.label),
|
||||
value: 'PREPARED',
|
||||
},
|
||||
{
|
||||
label: t(statusMap[1].label),
|
||||
label: t(reviewStatusMap.UNDERWAY.label),
|
||||
value: 'UNDERWAY',
|
||||
},
|
||||
{
|
||||
label: t(statusMap[2].label),
|
||||
label: t(reviewStatusMap.COMPLETED.label),
|
||||
value: 'COMPLETED',
|
||||
},
|
||||
{
|
||||
label: t(statusMap[3].label),
|
||||
label: t(reviewStatusMap.ARCHIVED.label),
|
||||
value: 'ARCHIVED',
|
||||
},
|
||||
],
|
||||
|
@ -310,7 +257,7 @@
|
|||
},
|
||||
{
|
||||
title: 'caseManagement.caseReview.type',
|
||||
dataIndex: 'type',
|
||||
dataIndex: 'reviewPassRule',
|
||||
type: FilterType.SELECT,
|
||||
selectProps: {
|
||||
mode: 'static',
|
||||
|
@ -328,7 +275,7 @@
|
|||
},
|
||||
{
|
||||
title: 'caseManagement.caseReview.reviewer',
|
||||
dataIndex: 'reviewer',
|
||||
dataIndex: 'reviewers',
|
||||
type: FilterType.SELECT,
|
||||
selectProps: {
|
||||
mode: 'static',
|
||||
|
@ -337,7 +284,7 @@
|
|||
},
|
||||
{
|
||||
title: 'caseManagement.caseReview.creator',
|
||||
dataIndex: 'creator',
|
||||
dataIndex: 'createUser',
|
||||
type: FilterType.SELECT,
|
||||
selectProps: {
|
||||
mode: 'static',
|
||||
|
@ -364,12 +311,17 @@
|
|||
},
|
||||
{
|
||||
title: 'caseManagement.caseReview.desc',
|
||||
dataIndex: 'desc',
|
||||
dataIndex: 'description',
|
||||
type: FilterType.INPUT,
|
||||
},
|
||||
{
|
||||
title: 'caseManagement.caseReview.cycle',
|
||||
dataIndex: 'cycle',
|
||||
title: 'caseManagement.caseReview.startTime',
|
||||
dataIndex: 'startTime',
|
||||
type: FilterType.DATE_PICKER,
|
||||
},
|
||||
{
|
||||
title: 'caseManagement.caseReview.endTime',
|
||||
dataIndex: 'endTime',
|
||||
type: FilterType.DATE_PICKER,
|
||||
},
|
||||
];
|
||||
|
@ -382,10 +334,10 @@
|
|||
const columns: MsTableColumn = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
dataIndex: 'num',
|
||||
sortIndex: 1,
|
||||
showTooltip: true,
|
||||
width: 90,
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: 'caseManagement.caseReview.name',
|
||||
|
@ -415,13 +367,14 @@
|
|||
},
|
||||
{
|
||||
title: 'caseManagement.caseReview.type',
|
||||
dataIndex: 'type',
|
||||
slotName: 'reviewPassRule',
|
||||
dataIndex: 'reviewPassRule',
|
||||
width: 90,
|
||||
},
|
||||
{
|
||||
title: 'caseManagement.caseReview.reviewer',
|
||||
dataIndex: 'reviewer',
|
||||
showTooltip: true,
|
||||
slotName: 'reviewers',
|
||||
dataIndex: 'reviewers',
|
||||
sortable: {
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
},
|
||||
|
@ -429,7 +382,7 @@
|
|||
},
|
||||
{
|
||||
title: 'caseManagement.caseReview.creator',
|
||||
dataIndex: 'creator',
|
||||
dataIndex: 'createUser',
|
||||
width: 90,
|
||||
},
|
||||
{
|
||||
|
@ -445,14 +398,14 @@
|
|||
},
|
||||
{
|
||||
title: 'caseManagement.caseReview.desc',
|
||||
dataIndex: 'desc',
|
||||
dataIndex: 'description',
|
||||
width: 150,
|
||||
showTooltip: true,
|
||||
},
|
||||
{
|
||||
title: 'caseManagement.caseReview.cycle',
|
||||
dataIndex: 'cycle',
|
||||
width: 340,
|
||||
width: 350,
|
||||
},
|
||||
{
|
||||
title: 'common.operation',
|
||||
|
@ -475,9 +428,9 @@
|
|||
(item) => {
|
||||
return {
|
||||
...item,
|
||||
type: t(typeMap[item.type as keyof typeof typeMap]),
|
||||
tags: item.tags?.map((e: string) => ({ id: e, name: e })) || [],
|
||||
cycle: `${dayjs(item.cycle[0]).format('YYYY-MM-DD HH:mm:ss')} - ${dayjs(item.cycle[1]).format(
|
||||
tags: (item.tags || []).map((e: string) => ({ id: e, name: e })),
|
||||
reviewers: item.reviewers.map((e: ReviewDetailReviewersItem) => e.userName),
|
||||
cycle: `${dayjs(item.startTime).format('YYYY-MM-DD HH:mm:ss')} - ${dayjs(item.endTime).format(
|
||||
'YYYY-MM-DD HH:mm:ss'
|
||||
)}`,
|
||||
};
|
||||
|
@ -492,19 +445,43 @@
|
|||
],
|
||||
};
|
||||
|
||||
function searchReview() {
|
||||
setLoadListParams({
|
||||
const tableQueryParams = ref<any>();
|
||||
function searchReview(filter?: FilterResult) {
|
||||
const params = {
|
||||
keyword: keyword.value,
|
||||
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();
|
||||
tableQueryParams.value = {
|
||||
...params,
|
||||
current: propsRes.value.msPagination?.current,
|
||||
pageSize: propsRes.value.msPagination?.pageSize,
|
||||
};
|
||||
emit('init', {
|
||||
...tableQueryParams.value,
|
||||
});
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
searchReview();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.showType,
|
||||
() => {
|
||||
searchReview();
|
||||
}
|
||||
);
|
||||
|
||||
const tableSelected = ref<(string | number)[]>([]);
|
||||
const batchParams = ref<BatchActionQueryParams>({
|
||||
selectedIds: [],
|
||||
|
@ -524,35 +501,9 @@
|
|||
const activeRecord = ref({
|
||||
id: '',
|
||||
name: '',
|
||||
status: 0,
|
||||
status: 'PREPARED' as ReviewStatus,
|
||||
});
|
||||
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 评审状态
|
||||
|
@ -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({
|
||||
type: 'warning',
|
||||
title: t('caseManagement.caseReview.archivedTitle', { name: record.name }),
|
||||
|
@ -670,7 +630,7 @@
|
|||
* 处理表格更多按钮事件
|
||||
* @param item
|
||||
*/
|
||||
function handleMoreActionSelect(item: ActionsItem, record: any) {
|
||||
function handleMoreActionSelect(item: ActionsItem, record: ReviewItem) {
|
||||
switch (item.eventTag) {
|
||||
case 'delete':
|
||||
activeRecord.value = record;
|
||||
|
|
|
@ -1,26 +1,5 @@
|
|||
<template>
|
||||
<MsColorLine
|
||||
: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"
|
||||
>
|
||||
<MsColorLine :color-data="colorData" :height="props.height" :radius="props.radius">
|
||||
<template #popoverContent>
|
||||
<table>
|
||||
<tr>
|
||||
|
@ -28,15 +7,13 @@
|
|||
<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
|
||||
).toFixed(2)}%`
|
||||
}}
|
||||
<span
|
||||
>({{
|
||||
`${props.reviewDetail.passCount + props.reviewDetail.failCount}/${props.reviewDetail.caseCount}`
|
||||
}})</span
|
||||
>
|
||||
<span>
|
||||
({{ `${props.reviewDetail.passCount + props.reviewDetail.unPassCount}/${props.reviewDetail.caseCount}` }})
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -54,7 +31,7 @@
|
|||
<div>{{ t('caseManagement.caseReview.fail') }}</div>
|
||||
</td>
|
||||
<td class="popover-value-td">
|
||||
{{ props.reviewDetail.failCount }}
|
||||
{{ props.reviewDetail.unPassCount }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -63,7 +40,7 @@
|
|||
<div>{{ t('caseManagement.caseReview.reReview') }}</div>
|
||||
</td>
|
||||
<td class="popover-value-td">
|
||||
{{ props.reviewDetail.reviewCount }}
|
||||
{{ props.reviewDetail.reviewedCount }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -72,7 +49,7 @@
|
|||
<div>{{ t('caseManagement.caseReview.reviewing') }}</div>
|
||||
</td>
|
||||
<td class="popover-value-td">
|
||||
{{ props.reviewDetail.reviewingCount }}
|
||||
{{ props.reviewDetail.underReviewedCount }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
@ -88,9 +65,9 @@
|
|||
const props = defineProps<{
|
||||
reviewDetail: {
|
||||
passCount: number;
|
||||
failCount: number;
|
||||
reviewCount: number;
|
||||
reviewingCount: number;
|
||||
unPassCount: number;
|
||||
reviewedCount: number;
|
||||
underReviewedCount: number;
|
||||
caseCount: number;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
@ -98,6 +75,41 @@
|
|||
radius?: string;
|
||||
}>();
|
||||
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>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
|
|
@ -7,30 +7,30 @@
|
|||
<script setup lang="ts">
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
export type StatusMap = 0 | 1 | 2 | 3;
|
||||
import type { ReviewStatus } from '@/models/caseManagement/caseReview';
|
||||
|
||||
const props = defineProps<{
|
||||
status: StatusMap;
|
||||
status: ReviewStatus;
|
||||
}>();
|
||||
const { t } = useI18n();
|
||||
|
||||
const statusMap = {
|
||||
0: {
|
||||
PREPARED: {
|
||||
label: 'caseManagement.caseReview.unStart',
|
||||
color: 'var(--color-text-n8)',
|
||||
class: '!text-[var(--color-text-1)]',
|
||||
},
|
||||
1: {
|
||||
UNDERWAY: {
|
||||
label: 'caseManagement.caseReview.going',
|
||||
color: 'rgb(var(--link-2))',
|
||||
class: '!text-[rgb(var(--link-6))]',
|
||||
},
|
||||
2: {
|
||||
COMPLETED: {
|
||||
label: 'caseManagement.caseReview.finished',
|
||||
color: 'rgb(var(--success-2))',
|
||||
class: '!text-[rgb(var(--success-6))]',
|
||||
},
|
||||
3: {
|
||||
ARCHIVED: {
|
||||
label: 'caseManagement.caseReview.archived',
|
||||
color: 'var(--color-text-n8)',
|
||||
class: '!text-[var(--color-text-4)]',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<MsCard
|
||||
:loading="loading"
|
||||
:title="isEdit ? t('menu.caseManagement.caseManagementCaseReviewEdit') : t('caseManagement.caseReview.create')"
|
||||
>
|
||||
<a-form ref="reviewFormRef" class="w-[732px]" :model="reviewForm" layout="vertical">
|
||||
|
@ -26,18 +27,19 @@
|
|||
/>
|
||||
</a-form-item>
|
||||
<a-form-item field="folderId" :label="t('caseManagement.caseReview.belongModule')">
|
||||
<a-select
|
||||
<a-tree-select
|
||||
v-model:modelValue="reviewForm.folderId"
|
||||
:placeholder="t('caseManagement.caseReview.belongModulePlaceholder')"
|
||||
:options="moduleOptions"
|
||||
:data="moduleOptions"
|
||||
class="w-[436px]"
|
||||
:field-names="{ title: 'name', key: 'id', children: 'children' }"
|
||||
:loading="moduleLoading"
|
||||
allow-search
|
||||
multiple
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item field="type" :label="t('caseManagement.caseReview.type')">
|
||||
<a-radio-group v-model:modelValue="reviewForm.type">
|
||||
<a-radio value="single">
|
||||
<a-radio-group v-model:modelValue="reviewForm.type" :disabled="isEdit">
|
||||
<a-radio value="SINGLE">
|
||||
<div class="flex items-center">
|
||||
{{ t('caseManagement.caseReview.single') }}
|
||||
<a-tooltip :content="t('caseManagement.caseReview.singleTip')" position="right">
|
||||
|
@ -48,7 +50,7 @@
|
|||
</a-tooltip>
|
||||
</div>
|
||||
</a-radio>
|
||||
<a-radio value="multi">
|
||||
<a-radio value="MULTIPLE">
|
||||
<div class="flex items-center">
|
||||
{{ t('caseManagement.caseReview.multi') }}
|
||||
<a-tooltip :content="t('caseManagement.caseReview.multiTip')" position="right">
|
||||
|
@ -83,10 +85,19 @@
|
|||
:placeholder="t('caseManagement.caseReview.reviewerPlaceholder')"
|
||||
:options="reviewersOptions"
|
||||
:search-keys="['label']"
|
||||
allow-clear
|
||||
allow-search
|
||||
multiple
|
||||
: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 field="tags" :label="t('caseManagement.caseReview.tag')">
|
||||
<MsTagsInput v-model:model-value="reviewForm.tags" />
|
||||
|
@ -95,6 +106,7 @@
|
|||
<a-range-picker
|
||||
v-model:model-value="reviewForm.cycle"
|
||||
show-time
|
||||
value-format="timestamp"
|
||||
:time-picker-props="{
|
||||
defaultValue: ['00:00:00', '00:00:00'],
|
||||
}"
|
||||
|
@ -105,8 +117,13 @@
|
|||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<div>{{ t('caseManagement.caseReview.pickCases') }}</div>
|
||||
<a-divider margin="4px" direction="vertical" />
|
||||
<MsButton type="text" :disabled="selectedAssociateCases.length === 0" @click="clearSelectedCases">
|
||||
<a-divider v-if="!isCopy" margin="4px" direction="vertical" />
|
||||
<MsButton
|
||||
v-if="!isCopy"
|
||||
type="text"
|
||||
:disabled="selectedAssociateCasesParams.selectIds.length === 0"
|
||||
@click="clearSelectedCases"
|
||||
>
|
||||
{{ t('caseManagement.caseReview.clearSelectedCases') }}
|
||||
</MsButton>
|
||||
</div>
|
||||
|
@ -114,10 +131,14 @@
|
|||
<div class="bg-[var(--color-text-n9)] p-[12px]">
|
||||
<div class="flex items-center">
|
||||
<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>
|
||||
<a-divider margin="8px" direction="vertical" />
|
||||
<MsButton type="text" class="font-medium" @click="caseAssociateVisible = true">
|
||||
<a-divider v-if="!isCopy" margin="8px" direction="vertical" />
|
||||
<MsButton v-if="!isCopy" type="text" class="font-medium" @click="caseAssociateVisible = true">
|
||||
{{ t('ms.case.associate.title') }}
|
||||
</MsButton>
|
||||
</div>
|
||||
|
@ -126,13 +147,15 @@
|
|||
</a-form>
|
||||
<template #footerRight>
|
||||
<div class="flex items-center">
|
||||
<a-button type="secondary" @click="cancelCreate">{{ t('common.cancel') }}</a-button>
|
||||
<a-button v-if="isEdit" type="primary" class="ml-[16px]" @click="updateReview">
|
||||
<a-button type="secondary" :disabled="saveLoading" @click="cancelCreate">{{ t('common.cancel') }}</a-button>
|
||||
<a-button v-if="isEdit" type="primary" class="ml-[16px]" :loading="saveLoading" @click="updateReview">
|
||||
{{ t('common.update') }}
|
||||
</a-button>
|
||||
<template v-else>
|
||||
<a-button type="secondary" class="mx-[16px]" @click="() => saveReview()">{{ t('common.save') }}</a-button>
|
||||
<a-button type="primary" @click="() => saveReview(true)">
|
||||
<a-button type="secondary" class="mx-[16px]" :loading="saveLoading" @click="() => saveReview()">
|
||||
{{ t('common.save') }}
|
||||
</a-button>
|
||||
<a-button type="primary" :disabled="saveLoading" @click="() => saveReview(true)">
|
||||
{{ t('caseManagement.caseReview.review') }}
|
||||
</a-button>
|
||||
</template>
|
||||
|
@ -154,16 +177,25 @@
|
|||
import { useRoute, useRouter } from 'vue-router';
|
||||
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 MsCard from '@/components/pure/ms-card/index.vue';
|
||||
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
|
||||
import MsSelect from '@/components/business/ms-select';
|
||||
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 useAppStore from '@/store/modules/app';
|
||||
|
||||
import type { BaseAssociateCaseRequest, ReviewPassRule } from '@/models/caseManagement/caseReview';
|
||||
import { CaseManagementRouteEnum } from '@/enums/routeEnum';
|
||||
|
||||
import type { FormInstance } from '@arco-design/web-vue';
|
||||
|
@ -174,30 +206,36 @@
|
|||
const { t } = useI18n();
|
||||
|
||||
const isEdit = ref(!!route.query.id);
|
||||
const isCopy = ref(!!route.query.copyId);
|
||||
const reviewFormRef = ref<FormInstance>();
|
||||
const reviewForm = ref({
|
||||
name: '',
|
||||
desc: '',
|
||||
folderId: '',
|
||||
type: 'single',
|
||||
reviewers: [],
|
||||
tags: [],
|
||||
cycle: [],
|
||||
folderId: (route.query.moduleId as string) || 'root',
|
||||
type: 'SINGLE' as ReviewPassRule,
|
||||
reviewers: [] as string[],
|
||||
tags: [] as string[],
|
||||
cycle: [] as number[],
|
||||
caseCount: 0,
|
||||
});
|
||||
const moduleOptions = ref([
|
||||
{
|
||||
label: '全部',
|
||||
value: 'all',
|
||||
},
|
||||
{
|
||||
label: '模块1',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: '模块2',
|
||||
value: '2',
|
||||
},
|
||||
]);
|
||||
const moduleOptions = ref<SelectOptionData[]>([]);
|
||||
const moduleLoading = ref(false);
|
||||
|
||||
/**
|
||||
* 初始化模块选择
|
||||
*/
|
||||
async function initModules() {
|
||||
try {
|
||||
moduleLoading.value = true;
|
||||
moduleOptions.value = await getReviewModules(appStore.currentProjectId);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
moduleLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const reviewersOptions = ref<SelectOptionData[]>([]);
|
||||
const reviewerLoading = ref(false);
|
||||
|
||||
|
@ -205,7 +243,7 @@
|
|||
try {
|
||||
reviewerLoading.value = true;
|
||||
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) {
|
||||
// eslint-disable-next-line no-console
|
||||
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[]) {
|
||||
selectedAssociateCases.value = [...ids];
|
||||
function writeAssociateCases(param: BaseAssociateCaseRequest) {
|
||||
selectedAssociateCasesParams.value = { ...param };
|
||||
}
|
||||
|
||||
function clearSelectedCases() {
|
||||
selectedAssociateCases.value = [];
|
||||
selectedAssociateCasesParams.value = {
|
||||
excludeIds: [],
|
||||
selectIds: [],
|
||||
selectAll: false,
|
||||
condition: {},
|
||||
moduleIds: [],
|
||||
versionId: '',
|
||||
refId: '',
|
||||
projectId: '',
|
||||
};
|
||||
}
|
||||
|
||||
function cancelCreate() {
|
||||
router.back();
|
||||
}
|
||||
|
||||
const saveLoading = ref(false);
|
||||
function saveReview(isGoReview = false) {
|
||||
reviewFormRef.value?.validate(async (errors) => {
|
||||
if (!errors) {
|
||||
Message.success(t('common.createSuccess'));
|
||||
if (isGoReview) {
|
||||
// 是否去评审,是的话先保存然后直接跳转至该评审详情页进行评审
|
||||
router.replace({
|
||||
name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW,
|
||||
});
|
||||
} else {
|
||||
router.replace({
|
||||
name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW,
|
||||
});
|
||||
try {
|
||||
saveLoading.value = true;
|
||||
const { name, folderId, type, cycle, tags, desc, reviewers } = reviewForm.value;
|
||||
let res = '';
|
||||
if (isCopy.value) {
|
||||
// 复制评审场景
|
||||
res = await copyReview({
|
||||
copyId: route.query.copyId as string,
|
||||
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() {
|
||||
reviewFormRef.value?.validate(async (errors) => {
|
||||
if (!errors) {
|
||||
Message.success(t('common.updateSuccess'));
|
||||
router.replace({
|
||||
name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW,
|
||||
});
|
||||
try {
|
||||
saveLoading.value = true;
|
||||
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 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(() => {
|
||||
initModules();
|
||||
initReviewers();
|
||||
if (isEdit.value || isCopy.value) {
|
||||
// 编辑评审场景、复制评审场景初始化评审数据
|
||||
initReviewDetail();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -269,4 +414,11 @@
|
|||
:deep(.arco-form-item-label-col) {
|
||||
@apply w-auto flex-none;
|
||||
}
|
||||
:deep(.reviewer-select) {
|
||||
.arco-select-view-tag {
|
||||
@apply rounded-full;
|
||||
|
||||
padding-left: 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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>
|
||||
<a-tooltip :content="reviewDetail.name">
|
||||
<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))]"
|
||||
>
|
||||
<MsIcon type="icon-icon-contacts" size="13" />
|
||||
{{ t('caseManagement.caseReview.single') }}
|
||||
{{
|
||||
reviewDetail.reviewPassRule === 'SINGLE'
|
||||
? t('caseManagement.caseReview.single')
|
||||
: t('caseManagement.caseReview.multi')
|
||||
}}
|
||||
</div>
|
||||
<statusTag :status="(reviewDetail.status as StatusMap)" 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"
|
||||
/>
|
||||
<statusTag :status="(reviewDetail.status as ReviewStatus)" class="mx-[16px]" />
|
||||
</template>
|
||||
<template #headerRight>
|
||||
<div class="mr-[16px] flex items-center">
|
||||
<a-switch v-model:model-value="onlyMine" size="small" class="mr-[8px]" />
|
||||
{{ t('caseManagement.caseReview.onlyMine') }}
|
||||
</div>
|
||||
<MsButton type="button" status="default">
|
||||
<MsButton type="button" status="default" @click="associateDrawerVisible = true">
|
||||
<MsIcon type="icon-icon_link-record_outlined1" class="mr-[8px]" />
|
||||
{{ t('ms.case.associate.title') }}
|
||||
</MsButton>
|
||||
<MsButton type="button" status="default">
|
||||
<MsButton type="button" status="default" @click="editReview">
|
||||
<MsIcon type="icon-icon_edit_outlined" class="mr-[8px]" />
|
||||
{{ t('common.edit') }}
|
||||
</MsButton>
|
||||
<MsButton type="button" status="default">
|
||||
<MsButton type="button" status="default" @click="copyReview">
|
||||
<MsIcon type="icon-icon_copy_outlined" class="mr-[8px]" />
|
||||
{{ t('common.copy') }}
|
||||
</MsButton>
|
||||
<MsButton type="button" status="default">
|
||||
<MsIcon type="icon-icon_collection_outlined" class="mr-[8px]" />
|
||||
{{ t('common.fork') }}
|
||||
<MsButton type="button" status="default" :loading="followLoading" @click="toggleFollowReview">
|
||||
<MsIcon
|
||||
: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>
|
||||
<MsTableMoreAction :list="moreAction">
|
||||
<MsTableMoreAction :list="moreAction" @select="handleMoreSelect">
|
||||
<MsButton type="button" status="default">
|
||||
<MsIcon type="icon-icon_more_outlined" class="mr-[8px]" />
|
||||
{{ t('common.more') }}
|
||||
|
@ -58,19 +54,17 @@
|
|||
<div class="mb-[4px] flex items-center gap-[24px]">
|
||||
<div class="text-[var(--color-text-4)]">
|
||||
<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 class="text-[var(--color-text-1)]">{{ reviewDetail.reviewCount }}/</span
|
||||
<span class="text-[var(--color-text-1)]"> {{ reviewDetail.reviewedCount }}/ </span
|
||||
>{{ reviewDetail.caseCount }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-[var(--color-text-4)]">
|
||||
<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 class="text-[var(--color-text-1)]">
|
||||
{{ ((reviewDetail.reviewCount / reviewDetail.caseCount) * 100).toFixed(2) }}%
|
||||
</span>
|
||||
<span class="text-[var(--color-text-1)]"> {{ reviewDetail.passRate }}% </span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -84,18 +78,39 @@
|
|||
</a-tabs>
|
||||
</div>
|
||||
</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>
|
||||
<template #first>
|
||||
<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>
|
||||
</template>
|
||||
<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>
|
||||
</MsSplitBox>
|
||||
</MsCard>
|
||||
<AssociateDrawer
|
||||
v-model:visible="associateDrawerVisible"
|
||||
v-model:project="associateDrawerProject"
|
||||
@success="writeAssociateCases"
|
||||
/>
|
||||
<deleteReviewModal v-model:visible="deleteModalVisible" :record="reviewDetail" @success="handleDeleteSuccess" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
@ -103,6 +118,7 @@
|
|||
* @description 功能测试-用例评审-评审详情
|
||||
*/
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
|
||||
import MsButton from '@/components/pure/ms-button/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 MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
|
||||
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 CaseTree from './components/detail/caseTree.vue';
|
||||
import deleteReviewModal from './components/index/deleteReviewModal.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 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 route = useRoute();
|
||||
const userStore = useUserStore();
|
||||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const loading = ref(false);
|
||||
const reviewDetail = ref({
|
||||
name: '具体的用例评审的名称,最大宽度260px,超过展示省略号啦',
|
||||
status: 2,
|
||||
caseCount: 100,
|
||||
passCount: 0,
|
||||
failCount: 10,
|
||||
reviewCount: 20,
|
||||
reviewingCount: 25,
|
||||
const reviewDetail = ref<ReviewItem>({
|
||||
...reviewDefaultDetail,
|
||||
});
|
||||
|
||||
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 moreAction = ref<ActionsItem[]>([]);
|
||||
const fullActions = [
|
||||
|
||||
const showTab = ref(0);
|
||||
const tabList = ref([
|
||||
{
|
||||
label: t('caseManagement.caseReview.archive'),
|
||||
eventTag: 'archive',
|
||||
icon: 'icon-icon-draft',
|
||||
key: 0,
|
||||
title: t('menu.caseManagement.featureCase'),
|
||||
},
|
||||
]);
|
||||
|
||||
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'),
|
||||
eventTag: 'export',
|
||||
icon: 'icon-icon_upload_outlined',
|
||||
label: t('caseManagement.caseReview.quickCreate'),
|
||||
eventTag: 'createCase',
|
||||
icon: 'icon-icon_add_outlined-1',
|
||||
},
|
||||
{
|
||||
label: t('caseManagement.caseReview.createTestPlan'),
|
||||
|
@ -161,52 +299,72 @@
|
|||
danger: true,
|
||||
},
|
||||
];
|
||||
|
||||
onBeforeMount(() => {
|
||||
if (reviewDetail.value.status === 2) {
|
||||
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');
|
||||
const moreAction = computed(() => {
|
||||
if (reviewDetail.value.status === 'COMPLETED') {
|
||||
return [...fullActions];
|
||||
}
|
||||
if (reviewDetail.value.status === 'ARCHIVED') {
|
||||
return fullActions.filter((e) => e.eventTag === 'delete');
|
||||
}
|
||||
return fullActions.filter((e) => e.eventTag !== 'archive');
|
||||
});
|
||||
|
||||
const showTab = ref(0);
|
||||
const tabList = ref([
|
||||
{
|
||||
key: 0,
|
||||
title: t('menu.caseManagement.featureCase'),
|
||||
},
|
||||
]);
|
||||
function handleMoreSelect(item: ActionsItem) {
|
||||
switch (item.eventTag) {
|
||||
case 'createCase':
|
||||
createCase();
|
||||
break;
|
||||
case 'delete':
|
||||
deleteModalVisible.value = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const pagination = ref({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
const followLoading = ref(false);
|
||||
async function toggleFollowReview() {
|
||||
try {
|
||||
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>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<MsCard simple no-content-padding>
|
||||
<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-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="reviewByMe">{{ t('caseManagement.caseReview.waitMyReview') }}</a-radio>
|
||||
<a-radio value="createByMe">{{ t('caseManagement.caseReview.myCreate') }}</a-radio>
|
||||
|
@ -12,11 +12,24 @@
|
|||
<MsSplitBox>
|
||||
<template #first>
|
||||
<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>
|
||||
</template>
|
||||
<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>
|
||||
</MsSplitBox>
|
||||
</div>
|
||||
|
@ -34,8 +47,10 @@
|
|||
import ModuleTree from './components/index/moduleTree.vue';
|
||||
import ReviewTable from './components/index/reviewTable.vue';
|
||||
|
||||
import { reviewModuleCount } from '@/api/modules/case-management/caseReview';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { ReviewListQueryParams } from '@/models/caseManagement/caseReview';
|
||||
import type { ModuleTreeNode } from '@/models/projectManagement/file';
|
||||
import { CaseManagementRouteEnum } from '@/enums/routeEnum';
|
||||
|
||||
|
@ -46,20 +61,29 @@
|
|||
|
||||
const showType = ref<ShowType>('all');
|
||||
|
||||
function changeShowType(val: string | number | boolean) {
|
||||
console.log('changeShowType', val);
|
||||
}
|
||||
|
||||
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 modulesCount = ref<Record<string, number>>({});
|
||||
|
||||
function initModuleTree(tree: ModuleTreeNode[]) {
|
||||
moduleTree.value = unref(tree);
|
||||
}
|
||||
|
||||
function handleFolderNodeSelect(ids: (string | number)[]) {
|
||||
function handleFolderNodeSelect(ids: string[], _offspringIds: string[]) {
|
||||
[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() {
|
||||
|
|
|
@ -65,7 +65,7 @@ export default {
|
|||
'caseManagement.caseReview.reviewNameRequired': 'Review name cannot be empty',
|
||||
'caseManagement.caseReview.descPlaceholder': 'Please describe this review',
|
||||
'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.defaultReviewer': 'Default reviewer',
|
||||
'caseManagement.caseReview.defaultReviewerRequired': 'The default reviewer cannot be empty',
|
||||
|
|
|
@ -60,7 +60,7 @@ export default {
|
|||
'caseManagement.caseReview.reviewNameRequired': '评审名称不能为空',
|
||||
'caseManagement.caseReview.descPlaceholder': '请对该评审进行描述',
|
||||
'caseManagement.caseReview.belongModule': '所属模块',
|
||||
'caseManagement.caseReview.belongModulePlaceholder': '请选择该用例所属模块',
|
||||
'caseManagement.caseReview.belongModulePlaceholder': '请选择该评审所属模块',
|
||||
'caseManagement.caseReview.reviewerPlaceholder': '请选择评审人',
|
||||
'caseManagement.caseReview.defaultReviewer': '默认评审人',
|
||||
'caseManagement.caseReview.defaultReviewerRequired': '默认评审人',
|
||||
|
@ -122,4 +122,13 @@ export default {
|
|||
'caseManagement.caseReview.crateCase': '创建用例',
|
||||
'caseManagement.caseReview.demandCases': '需求关联列表',
|
||||
'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': '更新用例',
|
||||
};
|
||||
|
|
|
@ -68,6 +68,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeMount, ref, watch } from 'vue';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
|
@ -88,7 +89,7 @@
|
|||
const props = defineProps<{
|
||||
isExpandAll: boolean;
|
||||
activeFolder?: string; // 当前选中的文件夹,弹窗模式下需要使用
|
||||
selectedKeys?: Array<string | number>; // 选中的节点 key
|
||||
selectedKeys: Array<string | number>; // 选中的节点 key
|
||||
isModal?: boolean; // 是否是弹窗模式
|
||||
modulesCount?: Record<string, number>; // 模块数量统计对象
|
||||
showType?: string; // 显示类型
|
||||
|
@ -131,21 +132,7 @@
|
|||
];
|
||||
const renamePopVisible = ref(false);
|
||||
|
||||
const selectedKeys = ref(props.selectedKeys || []);
|
||||
|
||||
watch(
|
||||
() => props.selectedKeys,
|
||||
(val) => {
|
||||
selectedKeys.value = val || [];
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => selectedKeys.value,
|
||||
(val) => {
|
||||
emit('update:selectedKeys', val);
|
||||
}
|
||||
);
|
||||
const selectedKeys = useVModel(props, 'selectedKeys', emit);
|
||||
|
||||
/**
|
||||
* 初始化模块树
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
allow-clear
|
||||
class="mr-[8px] w-[240px]"
|
||||
:prefix="t('project.messageManagement.robot')"
|
||||
value-key="id"
|
||||
:multiple="true"
|
||||
:has-all-select="true"
|
||||
:default-all-select="true"
|
||||
|
@ -77,6 +76,7 @@
|
|||
label: (val as Record<string, any>).name,
|
||||
value: val,
|
||||
})"
|
||||
:object-value="true"
|
||||
@remove="changeMessageReceivers(false, record, dataIndex as string)"
|
||||
@popup-visible-change="changeMessageReceivers($event, record, dataIndex as string)"
|
||||
/>
|
||||
|
@ -148,7 +148,7 @@
|
|||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const robotFilters = ref<RobotItem[]>([]);
|
||||
const robotFilters = ref<string[]>([]);
|
||||
const robotOptions = ref<(SelectOptionData & RobotItem)[]>([]);
|
||||
const fullRef = ref<HTMLElement | null>();
|
||||
|
||||
|
@ -193,7 +193,7 @@
|
|||
}
|
||||
const tempArr = [...staticColumns];
|
||||
for (let i = 0; i < robotFilters.value.length; i++) {
|
||||
const robotId = robotFilters.value[i].id;
|
||||
const robotId = robotFilters.value[i];
|
||||
tempArr.push({
|
||||
title: robotOptions.value.find((e) => e.id === robotId)?.label,
|
||||
dataIndex: robotId,
|
||||
|
@ -325,6 +325,7 @@
|
|||
.filter((e) => e.enable)
|
||||
.map((e) => ({
|
||||
label: e.name,
|
||||
value: e.id,
|
||||
...e,
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -568,4 +568,3 @@
|
|||
}
|
||||
}
|
||||
</style>
|
||||
@/components/business/ms-select/index
|
||||
|
|
Loading…
Reference in New Issue