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

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

View File

@ -3,31 +3,52 @@ import {
AddReviewModuleUrl,
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 });
};

View File

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

View File

@ -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 });
}

View File

@ -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'; // 评审详情-提交评审

View File

@ -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'; // 个人信息-获取三方平台账号信息-插件信息

View File

@ -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 {

View File

@ -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 {

View File

@ -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);

View File

@ -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;

View File

@ -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'],
}

View File

@ -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 筛选

View File

@ -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);
};

View File

@ -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'"

View File

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

View File

@ -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>

View File

@ -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 {

View File

@ -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,
};
}

View File

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

View File

@ -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

View File

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

View File

@ -38,7 +38,7 @@ export default function useSelect(config: UseSelectOption) {
let tagCount = 0;
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是标签的边距、边框等宽度

View File

@ -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',
};

View File

@ -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,

View File

@ -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': '默认模块',
};

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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,

View File

@ -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;
}

View File

@ -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 }}

View File

@ -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;

View File

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

View File

@ -1,12 +1,12 @@
<template>
<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>

View File

@ -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_DATAX_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';

View File

@ -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',
},

View File

@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

@ -7,10 +7,10 @@
allow-clear
/>
<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="{

View File

@ -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',
};

View File

@ -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': '自动',
};

View File

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

View File

@ -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;

View File

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

View File

@ -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[]>([]);
// localitem
@ -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,
});

View File

@ -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();
},
{

View File

@ -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;

View File

@ -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>

View File

@ -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>

View File

@ -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;
}
);

View File

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

View File

@ -199,6 +199,7 @@
hideMoreAction: e.id === 'root',
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;
}
);

View File

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

View File

@ -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;

View File

@ -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>

View File

@ -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)]',

View File

@ -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>

View File

@ -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>

View File

@ -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() {

View File

@ -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',

View File

@ -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': '更新用例',
};

View File

@ -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);
/**
* 初始化模块树

View File

@ -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,
}));
}

View File

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