feat(功能用例&测试计划): 功能用例附件联调和bug修改&测试计划模块展开行页面

This commit is contained in:
xinxin.wu 2024-01-24 13:39:15 +08:00 committed by 刘瑞斌
parent d96e9d2795
commit ab87dcca8a
47 changed files with 1725 additions and 226 deletions

View File

@ -22,6 +22,11 @@ export default mergeConfig(
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/front\/file/, ''),
},
'/attachment': {
target: 'http://172.16.200.18:8081/',
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/front\/attachment/, ''),
},
'/plugin/image': {
target: 'http://172.16.200.18:8081/',
changeOrigin: true,

View File

@ -1,3 +1,4 @@
import { MsFileItem } from '@/components/pure/ms-upload/types';
import { CommentItem, CommentParams } from '@/components/business/ms-comment/types';
import MSR from '@/api/http/index';
@ -27,6 +28,7 @@ import {
DownloadExcelTemplateUrl,
DownloadFileUrl,
EditorUploadFileUrl,
exportExcelCheckUrl,
FollowerCaseUrl,
GetAssociatedCaseIdsUrl,
GetAssociatedDebuggerUrl,
@ -80,6 +82,7 @@ import type {
DeleteCaseType,
DemandItem,
DetailCase,
ImportExcelType,
ModulesTreeType,
OperationFile,
PreviewImages,
@ -378,6 +381,12 @@ export function downloadTemplate(projectId: string, type: 'Excel' | 'Xmind') {
{ isTransformResponse: false }
);
}
// 导入excel文件检查
export function importExcelChecked(data: { request: ImportExcelType; fileList: File[] }) {
return MSR.uploadFile({ url: exportExcelCheckUrl }, { request: data.request, fileList: data.fileList }, '');
}
// 富文本编辑器上传图片文件
export function editorUploadFile(data: { fileList: File[] }) {
return MSR.uploadFile({ url: EditorUploadFileUrl }, { fileList: data.fileList }, '', false);

View File

@ -1,6 +1,7 @@
import MSR from '@/api/http/index';
import {
AddCommonScriptUrl,
ConnectionWebsocketUrl,
DeleteCommonScriptUrl,
GetCommonScriptDetailUrl,
GetCommonScriptPageUrl,
@ -9,12 +10,17 @@ import {
GetFormApiImportPageListUrl,
GetFormApiImportUrl,
GetInsertCommonScriptPageUrl,
TestScriptUrl,
UpdateCommonScriptUrl,
} from '@/api/requrls/project-management/commonScript';
import type { ModulesTreeType } from '@/models/caseManagement/featureCase';
import { CommonList, TableQueryParams } from '@/models/common';
import type { AddOrUpdateCommonScript, CommonScriptItem } from '@/models/projectManagement/commonScript';
import type {
AddOrUpdateCommonScript,
CommonScriptItem,
TestScriptType,
} from '@/models/projectManagement/commonScript';
// 获取公共脚本列表
export function getCommonScriptPage(data: TableQueryParams) {
@ -59,3 +65,21 @@ export function getFormApiImportPageList(data: TableQueryParams) {
export function getFormApiImportModuleCount(data: TableQueryParams) {
return MSR.post<Record<string, any>>({ url: GetFormApiImportModuleCountUrl, data });
}
// 测试脚本
export function testCommonScript(data: TestScriptType) {
return MSR.post({ url: TestScriptUrl, data });
}
// apiSocket 建立连接
export const apiSocket = (url: string) => {
let protocol = 'ws://';
if (window.location.protocol === 'https:') {
protocol = 'wss://';
}
const uri = protocol + window.location.host + url;
return new WebSocket(uri);
};
export function getSocket(reportId: string) {
return apiSocket(`${ConnectionWebsocketUrl}/${reportId}`);
}

View File

@ -0,0 +1,41 @@
import MSR from '@/api/http/index';
import {
addTestPlanModuleUrl,
DeleteTestPlanModuleUrl,
GetTestPlanModuleCountUrl,
GetTestPlanModuleUrl,
MoveTestPlanModuleUrl,
updateTestPlanModuleUrl,
} from '@/api/requrls/test-plan/testPlan';
import type { CreateOrUpdateModule, ModulesTreeType, UpdateModule } from '@/models/caseManagement/featureCase';
import type { CommonList, MoveModules, TableQueryParams } from '@/models/common';
// 获取模块树
export function getTestPlanModule(params: TableQueryParams) {
return MSR.get<ModulesTreeType[]>({ url: `${GetTestPlanModuleUrl}/${params.projectId}` });
}
// 创建模块树
export function createPlanModuleTree(data: CreateOrUpdateModule) {
return MSR.post({ url: addTestPlanModuleUrl, data });
}
// 更新模块树
export function updatePlanModuleTree(data: UpdateModule) {
return MSR.post({ url: updateTestPlanModuleUrl, data });
}
// 移动模块树
export function moveTestPlanModuleTree(data: MoveModules) {
return MSR.post({ url: MoveTestPlanModuleUrl, data });
}
// 删除模块
export function deletePlanModuleTree(id: string) {
return MSR.get({ url: `${DeleteTestPlanModuleUrl}/${id}` });
}
// 获取模块数量
export function getPlanModulesCounts(data: TableQueryParams) {
return MSR.post({ url: GetTestPlanModuleCountUrl, data });
}

View File

@ -138,6 +138,6 @@ export const DownloadExcelTemplateUrl = '/functional/case/download/excel/templat
// 富文本所需资源上传
export const EditorUploadFileUrl = '/attachment/upload/temp/file';
// 富文本资源详情预览压缩图
export const EditorPreviewImagesUrl = '/attachment/preview/compressed';
// 预览压缩图
export const PreviewEditorImageUrl = '/attachment/preview';
export const PreviewEditorImageUrl = '/attachment/download/file';
// 导入excel文件检查
export const exportExcelCheckUrl = '/functional/case/pre-check/excel';

View File

@ -18,3 +18,7 @@ export const GetFormApiImportUrl = '/api/definition/module/tree';
export const GetFormApiImportPageListUrl = '/api/definition/page';
// 获取公共脚本从api导入模块数量
export const GetFormApiImportModuleCountUrl = '/api/definition/module/count';
// 脚本测试
export const TestScriptUrl = '/api/test/custom/func/run';
// websoket连接
export const ConnectionWebsocketUrl = '/ws/api';

View File

@ -0,0 +1,12 @@
// 测试计划模块树
export const GetTestPlanModuleUrl = `/test-plan/module/tree`;
// 添加测试计划模块树
export const addTestPlanModuleUrl = `/test-plan/module/add`;
// 更新计划测试模块树
export const updateTestPlanModuleUrl = '/test-plan/module/update';
// 移动计划测试模块树
export const MoveTestPlanModuleUrl = '/test-plan/module/move';
// 删除计划测试模块树
export const DeleteTestPlanModuleUrl = '/test-plan/module/delete';
// 测试计划模块树数量
export const GetTestPlanModuleCountUrl = '/test-plan/module/count';

View File

@ -17,23 +17,23 @@
}>();
const caseLevelMap = {
P1: {
label: 'P1',
P0: {
label: 'P0',
bgColor: 'rgb(var(--danger-2))',
borderColor: 'rgb(var(--danger-6))',
},
P2: {
label: 'P2',
P1: {
label: 'P1',
bgColor: 'rgb(var(--warning-2))',
borderColor: 'rgb(var(--warning-6))',
},
P3: {
label: 'P3',
P2: {
label: 'P2',
bgColor: 'rgb(var(--link-2))',
borderColor: 'rgb(var(--link-5))',
},
P4: {
label: 'P4',
P3: {
label: 'P3',
bgColor: 'var(--color-text-n8)',
borderColor: 'var(--color-text-brand)',
},

View File

@ -1 +1 @@
export type CaseLevel = 'P1' | 'P2' | 'P3' | 'P4';
export type CaseLevel = 'P0' | 'P1' | 'P2' | 'P3';

View File

@ -55,7 +55,9 @@
<a-radio value="commonScript">{{ t('project.commonScript.commonScript') }}</a-radio>
<a-radio value="executionResult">{{ t('project.commonScript.executionResult') }}</a-radio>
</a-radio-group>
<a-button type="outline">{{ t('project.commonScript.scriptTest') }}</a-button>
<a-button type="outline" :loading="loading" @click="testScript">{{
t('project.commonScript.scriptTest')
}}</a-button>
</div>
<ScriptDefined
v-model:language="form.type"
@ -78,11 +80,14 @@
import ScriptDefined from './scriptDefined.vue';
import paramTable from '@/views/api-test/components/paramTable.vue';
import { getCommonScriptDetail } from '@/api/modules/project-management/commonScript';
import { getCommonScriptDetail, getSocket, testCommonScript } from '@/api/modules/project-management/commonScript';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import type { AddOrUpdateCommonScript, ParamsRequestType } from '@/models/projectManagement/commonScript';
const appStore = useAppStore();
const heightUsed = ref<number | undefined>(undefined);
const formRef = ref<FormInstance>();
const { t } = useI18n();
@ -120,7 +125,7 @@
projectId: '',
params: '',
script: '',
type: 'beanshellJSR223',
type: 'beanshell-jsr233',
result: '',
};
@ -140,7 +145,7 @@
{
title: 'project.commonScript.description',
slotName: 'desc',
dataIndex: 'description',
dataIndex: 'desc',
},
{
title: 'project.commonScript.isRequired',
@ -219,6 +224,69 @@
}
}
);
const loading = ref<boolean>(false);
const websocket = ref<any>();
function onDebugMessage(e: any) {
if (e.data && e.data.startsWith('result_')) {
try {
const data = e.data.substring(7);
websocket.value.close();
console.log(data, 'datadatadatadatadata');
} catch (error) {
websocket.value.close();
}
}
}
//
async function testScript() {
try {
loading.value = true;
const { type, script } = form.value;
const parameters = innerParams.value
.filter((item: any) => item.name && item.value)
.map((item) => {
return {
key: item.name,
value: item.value,
valid: item.mustContain,
};
});
const params = {
type,
script,
params: parameters,
projectId: appStore.currentProjectId,
};
const reportId = await testCommonScript(params);
if (reportId) {
websocket.value = getSocket(reportId);
// TODO
// websocket.value.addEventListener('open', (event) => {
// console.log('WebSocket:', event);
// websocket.value.send('Hello, WebSocket Server!');
// });
// websocket.value.addEventListener('message', (event) => {
// console.log(':', event.data);
// });
// websocket.value.addEventListener('close', (event) => {
// console.log('WebSocket:', event);
// });
// websocket.value.addEventListener('error', (event) => {
// console.error('WebSocket:', event);
// });
}
} catch (error) {
console.log(error);
} finally {
loading.value = false;
}
}
</script>
<style scoped></style>

View File

@ -70,7 +70,7 @@
const innerLanguageType = useVModel(props, 'languagesType', emit);
const languages = [
{ text: 'beanshellJSR223', value: 'beanshellJSR223' },
{ text: 'beanshellJSR223', value: 'beanshell-jsr233' },
{ text: 'beanshell', value: 'beanshell' },
{ text: 'python', value: 'python' },
{ text: 'groovy', value: 'groovy' },

View File

@ -5,7 +5,7 @@ import type { CommonScriptMenu } from '@/models/projectManagement/commonScript';
const { t } = useI18n();
export type Languages =
| 'beanshellJSR223'
| 'beanshell-jsr233'
| 'groovy'
| 'python'
| 'beanshell'
@ -518,7 +518,7 @@ export function getCodeTemplate(language: Languages, requestObj: any) {
return jsCode(requestObj);
case 'javascript':
return jsCode(requestObj);
case 'beanshellJSR223':
case 'beanshell-jsr233':
return javaCode(requestObj);
default:
return '';

View File

@ -13,19 +13,22 @@
* return unified().use(rehypeParse).use(rehypeFormat).use(rehypeStringify).processSync(content.value);
*/
import { useDebounceFn, useLocalStorage } from '@vueuse/core';
import { useVModel } from '@vueuse/core';
import type { MsFileItem } from '@/components/pure/ms-upload/types';
import AttachmentSelectorModal from './attachmentSelectorModal.vue';
import { editorUploadFile } from '@/api/modules/case-management/featureCase';
import { PreviewEditorImageUrl } from '@/api/requrls/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import useLocale from '@/locale/useLocale';
import { useAppStore } from '@/store';
import '@halo-dev/richtext-editor/dist/style.css';
import ExtensionImage from './extensions/image/index';
import suggestion from './extensions/mention/suggestion';
import {
type AnyExtension,
DecorationSet,
Editor,
Extension,
ExtensionAudio,
@ -66,6 +69,8 @@
ExtensionUnderline,
ExtensionVideo,
lowlight,
Plugin,
PluginKey,
RichTextEditor,
ToolbarItem,
ToolboxItem,
@ -74,12 +79,13 @@
import type { queueAsPromised } from 'fastq';
import * as fastq from 'fastq';
const appStore = useAppStore();
const { t } = useI18n();
// image drag and paste upload
type Task = {
file: File;
process: (compressUrl: string, permalink: string, fileId: string) => void;
process: (permalink: string, fileId: string) => void;
};
const props = withDefaults(
@ -87,6 +93,7 @@
raw?: string;
uploadImage?: (file: File) => Promise<any>;
maxHeight?: string;
filedIds?: string[];
}>(),
{
raw: '',
@ -98,100 +105,20 @@
const emit = defineEmits<{
(event: 'update:raw', value: string): void;
(event: 'update:filedIds', value: string[]): void;
(event: 'update', value: string): void;
}>();
/**
* 图片压缩
* @param {*} img 图片对象
* @param {*} type 图片类型
* @param {*} maxWidth 图片最大宽度
* @param {*} flag
*/
function compress(img, type, maxWidth, flag) {
let canvas: HTMLCanvasElement | null = document.createElement('canvas');
let ctx2: any = canvas.getContext('2d');
const ratio = img.width / img.height;
let { width } = img;
let { height } = img;
// flag
if (flag && maxWidth <= width) {
width = maxWidth;
height = maxWidth / ratio; //
}
canvas.width = width;
canvas.height = height;
ctx2.fillStyle = '#fff';
ctx2.fillRect(0, 0, canvas.width, canvas.height);
ctx2.drawImage(img, 0, 0, width, height);
let base64Data = canvas.toDataURL(type, 0.75);
if (type === 'image/gif') {
const regx = /(?<=data:image).*?(?=;base64)/; // replace
base64Data = base64Data.replace(regx, '/gif');
}
canvas = null;
ctx2 = null;
return base64Data;
}
function handleFile(file: File, callback: any, maxWidth = 600) {
if (!file || !/\/(?:png|jpg|jpeg|gif)/i.test(file.type)) {
return;
}
const reader = new FileReader();
// eslint-disable-next-line func-names
reader.onload = function () {
const { result } = this;
let img: HTMLImageElement | null = new Image();
img.onload = () => {
const compressedDataUrl = compress(img, file.type, maxWidth, true);
const url = compress(img, file.type, maxWidth, false);
img = null;
callback({
data: file,
compressedDataUrl,
url,
type: 'image',
});
};
img.src = result as any;
};
reader.readAsDataURL(file);
}
function onPaste(file: File) {
return new Promise((resovle, reject) => {
handleFile(file, (data) => {
resovle(data);
});
});
}
const imageMap = {};
const imagesNodes = useVModel(props, 'filedIds', emit);
async function asyncWorker(arg: Task): Promise<void> {
if (!props.uploadImage) {
return;
}
const uploadFileId = await props.uploadImage(arg.file);
const result: any = await onPaste(arg.file);
//
if (uploadFileId) {
// eslint-disable-next-line no-prototype-builtins
if (!imageMap.hasOwnProperty(uploadFileId)) {
imageMap[uploadFileId] = {
compressedUrl: result.compressedDataUrl,
permanentUrl: '',
fileId: uploadFileId,
};
}
arg.process(result.compressedDataUrl, imageMap[uploadFileId].permanentUrl, uploadFileId);
const permanentUrl = `${PreviewEditorImageUrl}/${appStore.currentProjectId}/${uploadFileId}/${true}`;
arg.process(permanentUrl, uploadFileId);
}
}
@ -214,6 +141,7 @@
const showSidebar = useLocalStorage('halo:editor:show-sidebar', true);
const attachmentSelectorModal = ref(false);
const selectedimagesNode = ref<string>();
onMounted(() => {
const debounceOnUpdate = useDebounceFn(() => {
@ -252,6 +180,32 @@
loading: 'lazy',
},
}),
Extension.create({
addProseMirrorPlugins() {
return [
new Plugin({
key: new PluginKey('imageBubbleMenu'),
props: {
decorations: (state) => {
const images: string[] = [];
const { doc } = state;
doc.descendants((node) => {
if (node.type.name === 'image') {
images.push(node.attrs.fileId);
}
});
imagesNodes.value = images;
if (!selectedimagesNode.value) {
// eslint-disable-next-line prefer-destructuring
selectedimagesNode.value = images[0];
}
return DecorationSet.empty;
},
},
}),
];
},
}),
ExtensionTaskList,
ExtensionLink.configure({
autolink: false,
@ -419,7 +373,7 @@
uploadQueue.push({
file,
// url url id
process: (compressUrl: string, permalink: string, fileId: string) => {
process: (permalink: string, fileId: string) => {
editor.value
?.chain()
.focus()
@ -427,9 +381,8 @@
{
type: 'image',
attrs: {
src: permalink,
fileId,
src: compressUrl,
permalinkSrc: permalink,
},
},
])

View File

@ -1,7 +1,6 @@
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue';
import { editorPreviewImages } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import Image from './index';
@ -59,15 +58,6 @@
},
});
const permalinkSrc = computed({
get: () => {
return props.node?.attrs.permalinkSrc;
},
set: (newPermalinkSrc: string) => {
props.updateAttributes({ permalinkSrc: newPermalinkSrc });
},
});
function handleSetFocus() {
props.editor.commands.setNodeSelection(props.getPos());
}
@ -145,14 +135,7 @@
height: node.attrs.height,
}"
>
<img
:src="src || permalinkSrc"
:title="node.attrs.title"
:alt="alt"
:href="href"
class="h-full w-full"
@load="onImageLoaded"
/>
<img :src="src" :title="node.attrs.title" :alt="alt" :href="href" class="h-full w-full" @load="onImageLoaded" />
</div>
</node-view-wrapper>
</template>

View File

@ -2,6 +2,7 @@ import { markRaw } from 'vue';
import ImageView from './ImageView.vue';
import { PreviewEditorImageUrl } from '@/api/requrls/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import type { ExtensionOptions, NodeBubbleMenu } from '../../types';
@ -97,7 +98,7 @@ const Image = TiptapImage.extend<ExtensionOptions & ImageOptions>({
},
renderHTML: (attributes) => {
return {
permalinkSrc: attributes.permalinkSrc,
permalinkSrc: attributes.src,
};
},
},

View File

@ -467,4 +467,21 @@ export const pathMap: PathMapItem[] = [
},
],
},
// 测试计划
{
key: 'TEST_PLAN', // 测试计划
locale: 'menu.testPlan',
route: RouteEnum.TEST_PLAN,
permission: [],
level: MENU_LEVEL[2],
children: [
{
key: 'TEST_PLAN_INDEX', // 测试计划-测试计划
locale: 'menu.testPlan',
route: RouteEnum.TEST_PLAN_INDEX,
permission: [],
level: MENU_LEVEL[2],
},
],
},
];

View File

@ -50,6 +50,7 @@ export enum ProjectManagementRouteEnum {
export enum TestPlanRouteEnum {
TEST_PLAN = 'testPlan',
TEST_PLAN_INDEX = 'testPlanIndex',
}
export enum UITestRouteEnum {

View File

@ -52,6 +52,7 @@ export enum TableKeyEnum {
PROJECT_MANAGEMENT_ENV_ENV_HTTP = 'projectManagementEnvEnvHttp',
PROJECT_MANAGEMENT_ENV_ALL_PARAM_HEADER = 'projectManagementEnvAllParamHeader',
PROJECT_MANAGEMENT_ENV_ALL_PARAM_VARIABLE = 'projectManagementEnvAllParamVariable',
TEST_PLAN_ALL_TABLE = 'testPlanAllTable',
}
// 具有特殊功能的列

View File

@ -307,3 +307,20 @@ export interface PreviewImages {
fileSource?: string; // 附件(ATTACHMENT)/功能用例详情(CASE_DETAIL)/用例评论(CASE_COMMENT)/评审评论(REVIEW_COMMENT)
local: boolean;
}
// 导入excel检查
export interface ImportExcelType {
projectId: string;
versionId: string;
cover: boolean;
}
export interface errorMessagesType {
rowNum: number;
errMsg: string;
}
export interface ValidateInfo {
failCount: number;
successCount: number;
errorMessages: errorMessagesType[];
}

View File

@ -50,3 +50,13 @@ export interface ParamsRequestType {
type: string;
value: string;
}
export interface TestScriptType {
type: string;
params: {
key: string;
value: string;
valid: boolean;
}[];
script: string;
projectId: string;
}

View File

@ -0,0 +1 @@
export default {};

View File

@ -6,7 +6,7 @@ import type { AppRouteRecordRaw } from '../types';
const TestPlan: AppRouteRecordRaw = {
path: '/test-plan',
name: TestPlanRouteEnum.TEST_PLAN,
redirect: '/test-plan/index',
redirect: '/test-plan/testPlanIndex',
component: DEFAULT_LAYOUT,
meta: {
locale: 'menu.testPlan',
@ -15,12 +15,15 @@ const TestPlan: AppRouteRecordRaw = {
hideChildrenInMenu: true,
},
children: [
// 测试计划
{
path: 'index',
name: 'testPlanIndex',
component: () => import('@/views/test-plan/index.vue'),
path: 'testPlanIndex',
name: TestPlanRouteEnum.TEST_PLAN_INDEX,
component: () => import('@/views/test-plan/testPlan/index.vue'),
meta: {
locale: 'menu.testPlan',
roles: ['*'],
isTopMenu: true,
},
},
],

View File

@ -360,6 +360,44 @@ export const downloadByteFile = (byte: BlobPart, fileName: string) => {
document.body.removeChild(link);
};
/**
*
* @param {*} img
* @param {*} type
* @param {*} maxWidth
* @param {*} flag
*/
export function compress(img, type, maxWidth, flag) {
let canvas: HTMLCanvasElement | null = document.createElement('canvas');
let ctx2: any = canvas.getContext('2d');
const ratio = img.width / img.height;
let { width } = img;
let { height } = img;
// 根据flag判断是否压缩图片
if (flag && maxWidth <= width) {
width = maxWidth;
height = maxWidth / ratio; // 维持图片宽高比
}
canvas.width = width;
canvas.height = height;
ctx2.fillStyle = '#fff';
ctx2.fillRect(0, 0, canvas.width, canvas.height);
ctx2.drawImage(img, 0, 0, width, height);
let base64Data = canvas.toDataURL(type, 0.75);
if (type === 'image/gif') {
const regx = /(?<=data:image).*?(?=;base64)/; // 正则表示时在用于replace时根据浏览器的不同有的需要为字符串
base64Data = base64Data.replace(regx, '/gif');
}
canvas = null;
ctx2 = null;
return base64Data;
}
/**
*
* @param str

View File

@ -166,7 +166,7 @@
};
formItem.value?.forEach((item: any) => {
customField.fieldId = item.field;
customField.value = JSON.stringify(item.value);
customField.value = Array.isArray(item.value) ? JSON.stringify(item.value) : item.value;
});
const params: BatchEditCaseType = {
selectIds: props.batchParams.selectedIds as string[],

View File

@ -234,6 +234,7 @@
import type { CustomAttributes, DetailCase, TabItemType } from '@/models/caseManagement/featureCase';
import { CaseManagementRouteEnum } from '@/enums/routeEnum';
import { getCaseLevels } from './utils';
import { LabelValue } from '@arco-design/web-vue/es/tree-select/interface';
import debounce from 'lodash-es/debounce';
@ -311,14 +312,11 @@
const detailInfo = ref<DetailCase>({ ...initDetail });
const customFields = ref<CustomAttributes[]>([]);
const caseLevels = ref<CaseLevel>('P1');
const caseLevels = ref<CaseLevel>('P0');
function loadedCase(detail: DetailCase) {
detailInfo.value = { ...detail };
customFields.value = detailInfo.value.customFields;
const caseLevelsValue = customFields.value.find((item) => item.fieldName === '用例等级')?.defaultValue;
if (caseLevelsValue) {
caseLevels.value = caseLevelsValue as CaseLevel;
}
caseLevels.value = getCaseLevels(customFields.value) as CaseLevel;
}
const moduleName = computed(() => {

View File

@ -35,7 +35,7 @@
<a-button type="text" class="px-0" @click="showCaseDetail(record.id, rowIndex)">{{ record.name }}</a-button>
</template>
<template #caseLevel="{ record }">
<caseLevel :case-level="getCaseLevel(record)" />
<caseLevel :case-level="getCaseLevels(record.customFields)" />
</template>
<template #reviewStatus="{ record }">
<MsIcon
@ -186,22 +186,15 @@
import { FilterFormItem, FilterResult, FilterType } from '@/components/pure/ms-advance-filter/type';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import { FieldTypeFormRules } from '@/components/pure/ms-form-create/form-create';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MinderEditor from '@/components/pure/ms-minder-editor/minderEditor.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type {
BatchActionParams,
BatchActionQueryParams,
MsTableColumn,
MsTableColumnData,
} from '@/components/pure/ms-table/type';
import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import type { TagType, Theme } from '@/components/pure/ms-tag/ms-tag.vue';
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
import BatchEditModal from './batchEditModal.vue';
import CaseDetailDrawer from './caseDetailDrawer.vue';
import FeatureCaseTree from './caseTree.vue';
@ -236,7 +229,7 @@
import { CaseManagementRouteEnum } from '@/enums/routeEnum';
import { ColumnEditTypeEnum, TableKeyEnum } from '@/enums/tableEnum';
import { getReviewStatusClass, getStatusText } from './utils';
import { getCaseLevels, getReviewStatusClass, getStatusText } from './utils';
import { LabelValue } from '@arco-design/web-vue/es/tree-select/interface';
const { openModal } = useModal();
@ -259,7 +252,7 @@
(e: 'import', type: 'Excel' | 'Xmind'): void;
}>();
const keyword = ref<string>();
const keyword = ref<string>('');
const filterRowCount = ref(0);
const showType = ref<string>('list');
@ -1061,11 +1054,6 @@
}
}
function getCaseLevel(record: CaseManagementTable): CaseLevel {
const caseLevelItem = record.customFields.find((it: any) => it.fieldName === '用例等级');
return caseLevelItem?.options.find((it: any) => it.value === caseLevelItem.defaultValue).text;
}
//
async function handleChangeModule(
record: CaseManagementTable,

View File

@ -18,7 +18,11 @@
></a-input>
</a-form-item>
<a-form-item field="precondition" :label="t('system.orgTemplate.precondition')" asterisk-position="end">
<MsRichText v-model:raw="form.prerequisite" :upload-image="handleUploadImage" />
<MsRichText
v-model:raw="form.prerequisite"
v-model:filed-ids="prerequisiteFileIds"
:upload-image="handleUploadImage"
/>
</a-form-item>
<a-form-item
field="step"
@ -48,17 +52,30 @@
<AddStep v-model:step-list="stepData" :is-disabled="false" />
</div>
<!-- 文本描述 -->
<MsRichText v-else v-model:raw="form.textDescription" :upload-image="handleUploadImage" />
<MsRichText
v-else
v-model:raw="form.textDescription"
v-model:filed-ids="textDescriptionFileIds"
:upload-image="handleUploadImage"
/>
</a-form-item>
<a-form-item
v-if="form.caseEditType === 'TEXT'"
field="remark"
:label="t('caseManagement.featureCase.expectedResult')"
>
<MsRichText v-model:raw="form.expectedResult" :upload-image="handleUploadImage" />
<MsRichText
v-model:raw="form.expectedResult"
v-model:filed-ids="expectedResultFileIds"
:upload-image="handleUploadImage"
/>
</a-form-item>
<a-form-item field="remark" :label="t('caseManagement.featureCase.remark')">
<MsRichText v-model:raw="form.description" :upload-image="handleUploadImage" />
<a-form-item field="description" :label="t('caseManagement.featureCase.remark')">
<MsRichText
v-model:raw="form.description"
v-model:filed-ids="descriptionFileIds"
:upload-image="handleUploadImage"
/>
</a-form-item>
<AddAttachment @change="handleChange" @link-file="associatedFile" @upload="beforeUpload" />
</a-form>
@ -696,6 +713,34 @@
}
}
// id
const prerequisiteFileIds = ref<string[]>([]);
// id
const textDescriptionFileIds = ref<string[]>([]);
// id
const expectedResultFileIds = ref<string[]>([]);
// id
const descriptionFileIds = ref<string[]>([]);
// id
const allAttachmentsFileIds = computed(() => {
return [
...prerequisiteFileIds.value,
...textDescriptionFileIds.value,
...expectedResultFileIds.value,
...descriptionFileIds.value,
];
});
watch(
() => allAttachmentsFileIds.value,
(val) => {
if (val) {
params.value.request.caseDetailFileIds = val;
}
}
);
async function handleUploadImage(file: File) {
const { data } = await editorUploadFile({
fileList: [file],

View File

@ -293,7 +293,7 @@
parentId: focusNodeKey.value,
};
await createCaseModuleTree(params);
Message.success(t('caseManagement.featureCase.addSuccess'));
Message.success(t('common.addSuccess'));
if (cancel) {
cancel();
}
@ -314,7 +314,7 @@
name: formValue?.field as string,
};
await updateCaseModuleTree(params);
Message.success(t('caseManagement.featureCase.addSuccess'));
Message.success(t('common.updateSuccess'));
if (cancel) {
cancel();
}

View File

@ -99,7 +99,7 @@
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
(e: 'save', files: FileItem[]): void;
(e: 'save', files: FileItem[], isRecover: boolean): void;
(e: 'close'): void;
}>();
const fileList = ref<FileItem[]>([]);
@ -141,7 +141,7 @@
}
function saveConfirm() {
emit('save', fileList.value);
emit('save', fileList.value, isRecover.value);
}
</script>

View File

@ -17,7 +17,7 @@
<div class="flex w-[92%] flex-col">
<span class="text-[var(--color-text-1)]">{{ t('caseManagement.featureCase.verifyingTemplate') }}</span>
<a-progress :percent="percent" size="large" />
<a-progress :percent="props.percent" size="large" />
</div>
</div>
<template #footer>
@ -37,6 +37,7 @@
const props = defineProps<{
visible: boolean;
validateType: 'Excel' | 'Xmind';
percent: number;
}>();
const emit = defineEmits<{
@ -53,8 +54,15 @@
const handleCancel = () => {
dialogVisible.value = false;
};
const percent = ref<number>(0.1);
watch(
() => props.percent,
(val) => {
if (val === 1) {
handleCancel();
emit('checkFinished');
}
}
);
</script>
<style scoped></style>

View File

@ -14,27 +14,36 @@
<div class="leading-8">
<span
>{{ t('caseManagement.featureCase.successfulCheck')
}}<span class="mx-1 text-[rgb(var(--success-6))]"> {{ validateCount.success }}</span
}}<span class="mx-1 text-[rgb(var(--success-6))]"> {{ validateResultInfo.successCount }}</span
>{{ t('caseManagement.featureCase.caseCount') }}</span
>
<span
>{{ t('caseManagement.featureCase.failCheck')
}}<span class="mx-1 font-medium text-[rgb(var(--danger-6))]">{{ validateCount.failure }}</span
}}<span class="mx-1 font-medium text-[rgb(var(--danger-6))]">{{ validateResultInfo.failCount }}</span
>{{ t('caseManagement.featureCase.caseCount') }}</span
>
<a-popover position="bottom">
<span v-if="validateCount.failure" class="font-medium text-[rgb(var(--primary-5))]">{{
<span v-if="validateResultInfo.failCount" class="font-medium text-[rgb(var(--primary-5))]">{{
t('caseManagement.featureCase.viewErrorDetail')
}}</span>
<template #title>
<div class="w-[440px]"
>{{ t('caseManagement.featureCase.someCaseImportFailed') }}
<span class="text-[var(--color-text-4)]">({{ validateCount.failure }})</span></div
<span class="text-[var(--color-text-4)]">({{ validateResultInfo.failCount }})</span></div
>
</template>
<template #content>
<div class="w-[440px]">
<a-divider class="mx-0 my-0" />
<div class="max-h-[400px] overflow-hidden">
<div
v-for="(item, index) of validateResultInfo.errorMessages"
:key="`${item.rowNum}-${index}`"
class="errorMessages"
>
{{ item.errMsg }}
</div>
</div>
<a-button class="mt-[8px]" type="text" long @click="showMore">{{
t('caseManagement.featureCase.ViewMore')
}}</a-button>
@ -47,18 +56,22 @@
<template #footer>
<div class="flex justify-end">
<MsButton
v-if="!validateCount.success || !validateCount.failure"
v-if="!validateResultInfo.successCount || !validateResultInfo.failCount"
type="text"
class="!text-[var(--color-text-1)]"
@click="backCaseList"
>{{ t('caseManagement.featureCase.backCaseList') }}</MsButton
>
<MsButton v-if="!validateCount.failure" type="text" class="ml-[8px]">{{
<MsButton v-if="!validateResultInfo.failCount" type="text" class="ml-[8px]" @click="confirmImport">{{
t('caseManagement.featureCase.import')
}}</MsButton>
<MsButton v-if="validateCount.failure || (validateCount.failure && validateCount.success)" class="ml-[8px]">{{
t('caseManagement.featureCase.backToUploadPage')
}}</MsButton>
<MsButton v-if="validateCount.failure && validateCount.success">{{
<MsButton
v-if="validateResultInfo.failCount || (validateResultInfo.failCount && validateResultInfo.successCount)"
class="ml-[8px]"
@click="handleCancel"
>{{ t('caseManagement.featureCase.backToUploadPage') }}</MsButton
>
<MsButton v-if="validateResultInfo.failCount && validateResultInfo.successCount" @click="confirmImport">{{
t('caseManagement.featureCase.ignoreErrorContinueImporting')
}}</MsButton>
</div>
@ -66,25 +79,29 @@
</a-modal>
<MsDrawer
v-model:visible="showMoreFailureCase"
:title="t('caseManagement.featureCase.cancelValidateSuccess', { number: validateCount.failure })"
:title="t('caseManagement.featureCase.cancelValidateSuccess', { number: validateResultInfo.failCount })"
:width="960"
:footer="false"
no-content-padding
>
<MsList
mode="static"
:virtual-list-props="{
height: 'calc(100vh - 325px)',
height: 'calc(100vh - 56px)',
}"
:data="failureCaseList"
:data="validateResultInfo.errorMessages"
:bordered="false"
:item-border="false"
:split="false"
:empty-text="t('project.fileManagement.noStorage')"
item-key-field="id"
class="mr-[-6px]"
active-item-class="activeItemClass"
item-class="my-[8px]"
:item-height="26"
>
<template #title="{ item, index }">
<div :key="index">
<div>{{ item }} </div>
<div :key="index" class="flex px-4">
<div class="circle"></div>
<div class="text-[var(--color-text-2)]">{{ item.errMsg }} </div>
</div>
</template>
</MsList>
@ -100,12 +117,15 @@
import { useI18n } from '@/hooks/useI18n';
import { ValidateInfo } from '@/models/caseManagement/featureCase';
import type { FileItem } from '@arco-design/web-vue';
const { t } = useI18n();
const props = defineProps<{
visible: boolean;
validateType: 'Excel' | 'Xmind';
validateInfo: ValidateInfo;
}>();
const emit = defineEmits<{
@ -123,24 +143,20 @@
validateResultModal.value = false;
}
const validateCount = ref({
success: 100,
failure: 100,
});
const validateResultInfo = ref<ValidateInfo>(props.validateInfo);
const validateResult = ref('');
const getIconType = computed(() => {
const { success, failure } = validateCount.value;
if (failure && success) {
const { successCount, failCount } = validateResultInfo.value;
if (failCount && successCount) {
validateResult.value = t('caseManagement.featureCase.partialCheckFailure');
return 'icon-icon_warning_colorful';
}
if (!failure) {
if (!failCount) {
validateResult.value = t('caseManagement.featureCase.CheckSuccess');
return 'icon-icon_succeed_colorful';
}
if (!success) {
if (!successCount) {
validateResult.value = t('caseManagement.featureCase.CheckFailure');
return 'icon-icon_close_colorful';
}
@ -148,9 +164,42 @@
const showMoreFailureCase = ref<boolean>(false);
const failureCaseList = ref([]);
//
function showMore() {}
function showMore() {
showMoreFailureCase.value = true;
}
//
function backCaseList() {
emit('close');
}
//
function confirmImport() {}
watchEffect(() => {
validateResultInfo.value = { ...props.validateInfo };
});
</script>
<style scoped></style>
<style scoped lang="less">
.activeItemClass {
background: none;
}
:deep(.ms-list-item--focus) {
background: none !important;
}
.errorMessages {
font-size: 14px;
line-height: 21px;
color: var(--color-text-2);
@apply my-4;
}
.circle {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--color-text-input-border);
@apply mr-2 mt-2;
}
</style>

View File

@ -85,7 +85,7 @@
@batch-action="handleTableBatch"
>
<template #caseLevel="{ record }">
<caseLevel :case-level="getCaseLevel(record)" />
<caseLevel :case-level="(getCaseLevels(record.customFields) as CaseLevel)" />
</template>
<template #reviewStatus="{ record }">
<MsIcon
@ -188,7 +188,7 @@
import { ModuleTreeNode } from '@/models/projectManagement/file';
import { TableKeyEnum } from '@/enums/tableEnum';
import { getReviewStatusClass, getStatusText } from './utils';
import { getCaseLevels, getReviewStatusClass, getStatusText } from './utils';
const tableStore = useTableStore();
const featureCaseStore = useFeatureCaseStore();
@ -252,11 +252,9 @@
},
{
title: 'caseManagement.featureCase.tableColumnLevel',
dataIndex: 'level',
slotName: 'caseLevel',
showInTable: true,
width: 200,
showTooltip: true,
ellipsis: true,
showDrag: true,
},
{
@ -790,16 +788,6 @@
initRecycleList();
};
//
function isCaseLevel(slotFieldId: string) {
const currentItem = initDefaultFields.value.find((item: any) => item.fieldId === slotFieldId);
return {
name: currentItem?.fieldName,
type: currentItem?.type,
options: currentItem?.options,
};
}
//
function getCaseState(caseState: string | undefined): { type: TagType; theme: Theme } {
switch (caseState) {

View File

@ -18,6 +18,7 @@
<MsRichText
v-if="isEditPreposition"
v-model:raw="detailForm.prerequisite"
v-model:filed-ids="prerequisiteFileIds"
:upload-image="handleUploadImage"
class="mt-2"
/>
@ -56,6 +57,7 @@
<MsRichText
v-if="detailForm.caseEditType === 'TEXT' && isEditPreposition"
v-model:raw="detailForm.textDescription"
v-model:filed-ids="textDescriptionFileIds"
:upload-image="handleUploadImage"
/>
<div v-if="detailForm.caseEditType === 'TEXT' && !isEditPreposition">{{
@ -70,12 +72,18 @@
<MsRichText
v-if="detailForm.caseEditType === 'TEXT' && isEditPreposition"
v-model:raw="detailForm.expectedResult"
v-model:filed-ids="expectedResultFileIds"
:upload-image="handleUploadImage"
/>
<div v-else class="text-[var(--color-text-3)]" v-html="detailForm.description || '-'"></div>
</a-form-item>
<a-form-item field="remark" :label="t('caseManagement.featureCase.remark')">
<MsRichText v-if="isEditPreposition" v-model:raw="detailForm.description" :upload-image="handleUploadImage" />
<a-form-item field="description" :label="t('caseManagement.featureCase.remark')">
<MsRichText
v-if="isEditPreposition"
v-model:filed-ids="descriptionFileIds"
v-model:raw="detailForm.description"
:upload-image="handleUploadImage"
/>
<div v-else v-dompurify-html="detailForm.description || '-'" class="text-[var(--color-text-3)]"></div>
</a-form-item>
<div v-if="isEditPreposition" class="flex justify-end">
@ -395,6 +403,25 @@
);
});
// id
const prerequisiteFileIds = ref<string[]>([]);
// id
const textDescriptionFileIds = ref<string[]>([]);
// id
const expectedResultFileIds = ref<string[]>([]);
// id
const descriptionFileIds = ref<string[]>([]);
// id
const allAttachmentsFileIds = computed(() => {
return [
...prerequisiteFileIds.value,
...textDescriptionFileIds.value,
...expectedResultFileIds.value,
...descriptionFileIds.value,
];
});
//
function getParams() {
const steps = stepData.value.map((item, index) => {
@ -420,6 +447,7 @@
unLinkFilesIds: unLinkFilesIds.value,
newAssociateFileListIds: newAssociateFileListIds.value,
customFields: customFieldsArr,
caseDetailFileIds: allAttachmentsFileIds.value,
},
fileList: fileList.value.filter((item: any) => item.status === 'init'), //
};

View File

@ -1,8 +1,9 @@
import type { MsFileItem } from '@/components/pure/ms-upload/types';
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
import { useI18n } from '@/hooks/useI18n';
import type { AssociatedList } from '@/models/caseManagement/featureCase';
import type { AssociatedList, CustomAttributes } from '@/models/caseManagement/featureCase';
import { StatusType } from '@/enums/caseEnum';
const { t } = useI18n();
@ -122,4 +123,13 @@ export function convertToFile(fileInfo: AssociatedList): MsFileItem {
};
}
// 返回用例等级
export function getCaseLevels(customFields: CustomAttributes[]): CaseLevel {
const caseLevelItem = customFields.find((it: any) => it.internal && it.fieldName === '用例等级');
return (
(caseLevelItem?.options.find((it: any) => it.value === caseLevelItem.defaultValue)?.text as CaseLevel) ||
('P0' as CaseLevel)
);
}
export default {};

View File

@ -104,10 +104,16 @@
<ValidateModal
v-model:visible="validateModal"
:validate-type="validateType"
:percent="progress"
@cancel="cancelValidate"
@check-finished="checkFinished"
/>
<ValidateResult v-model:visible="validateResultModal" :validate-type="validateType" />
<ValidateResult
v-model:visible="validateResultModal"
:validate-type="validateType"
:validate-info="validateInfo"
@close="closeHandler"
/>
</template>
<script setup lang="ts">
@ -127,12 +133,12 @@
import ValidateModal from './components/export/validateModal.vue';
import ValidateResult from './components/export/validateResult.vue';
import { createCaseModuleTree } from '@/api/modules/case-management/featureCase';
import { createCaseModuleTree, importExcelChecked } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import useFeatureCaseStore from '@/store/modules/case/featureCase';
import type { CaseModuleQueryParams, CreateOrUpdateModule } from '@/models/caseManagement/featureCase';
import type { CaseModuleQueryParams, CreateOrUpdateModule, ValidateInfo } from '@/models/caseManagement/featureCase';
import { CaseManagementRouteEnum } from '@/enums/routeEnum';
import type { FileItem } from '@arco-design/web-vue';
@ -268,8 +274,8 @@
});
const showExcelModal = ref<boolean>(false);
const validateType = ref<'Excel' | 'Xmind'>('Excel');
// excel
function importCase(type: 'Excel' | 'Xmind') {
validateType.value = type;
@ -277,15 +283,65 @@
}
const validateModal = ref<boolean>(false);
const fileList = ref<FileItem[]>([]);
//
function validateTemplate(files: FileItem[]) {
fileList.value = files;
validateModal.value = true;
}
//
const validateResultModal = ref<boolean>(false);
const validateInfo = ref<ValidateInfo>({
failCount: 0,
successCount: 0,
errorMessages: [],
});
const intervalId = ref<any>(null);
const progress = ref<number>(0);
const increment = ref<number>(0.1);
function updateProgress() {
progress.value = Math.floor(progress.value + increment.value);
if (progress.value >= 1) {
progress.value = 1;
}
}
function finish() {
clearInterval(intervalId.value);
progress.value = 1;
updateProgress();
}
function start() {
progress.value = 0;
increment.value = 0.1;
intervalId.value = setInterval(() => {
if (progress.value >= 1) {
finish();
} else {
updateProgress();
}
}, 100);
}
//
async function validateTemplate(files: FileItem[], cover: boolean) {
try {
start();
validateModal.value = true;
const params = {
projectId: appStore.currentProjectId,
versionId: '',
cover,
};
if (validateType.value === 'Excel') {
const result = await importExcelChecked({ request: params, fileList: files.map((item: any) => item.file) });
finish();
validateInfo.value = result.data;
}
} catch (error) {
console.log(error);
}
}
function checkFinished() {
validateResultModal.value = true;
}
@ -294,6 +350,11 @@
validateModal.value = false;
}
function closeHandler() {
showExcelModal.value = false;
validateResultModal.value = false;
}
onMounted(() => {
if (featureCaseStore.operatingState) {
[activeFolder.value] = featureCaseStore.moduleId;

View File

@ -64,5 +64,7 @@ export default {
paramEnvironmentSetGlobalVariable: 'Set run environment param',
insertPublicScript: 'Insert the public script',
terminationTest: 'Termination test',
code_hide_report_length: 'Hide report length',
code_add_report_length: 'Add report length to head',
},
};

View File

@ -66,6 +66,8 @@ export default {
paramEnvironmentSetGlobalVariable: '设置环境参数',
insertPublicScript: '插入公共脚本',
terminationTest: '终止测试',
code_hide_report_length: '隐藏报文长度',
code_add_report_length: '报文头添加长度',
},
},
};

View File

@ -17,6 +17,7 @@
:label="t('system.orgTemplate.fieldName')"
:rules="[{ required: true, message: t('system.orgTemplate.fieldNameRules') }]"
required
:disabled="fieldForm.internal"
asterisk-position="end"
>
<a-input

View File

@ -1,3 +0,0 @@
<template> TestPlan is waiting for development </template>
<script setup></script>

View File

@ -0,0 +1,408 @@
<template>
<ms-base-table
v-if="showType === 'list'"
v-bind="propsRes"
ref="tableRef"
class="mt-4"
:action-config="tableBatchActions"
:expanded-keys="expandedKeys"
@selected-change="handleTableSelect"
v-on="propsEvent"
@batch-action="handleTableBatch"
>
<template #num="{ record }">
<div v-if="(record.children || []).length > 0" class="mr-2 flex items-center" @click="expandHandler(record)">
<MsIcon
type="icon-icon_split-turn-down-left"
class="arrowIcon mr-1 text-[16px]"
:class="getIconClass(record)"
/>
<span :class="getIconClass(record)">{{ (record.children || []).length || 0 }}</span>
</div>
<span>{{ record.id }}</span>
</template>
<template #statusFilter="{ columnConfig }">
<a-trigger v-model:popup-visible="statusFilterVisible" trigger="click" @popup-visible-change="handleFilterHidden">
<a-button type="text" class="arco-btn-text--secondary" @click="statusFilterVisible = true">
{{ t(columnConfig.title as string) }}
<icon-down :class="statusFilterVisible ? 'text-[rgb(var(--primary-5))]' : ''" />
</a-button>
<template #content>
<div class="arco-table-filters-content">
<div class="flex items-center justify-center px-[6px] py-[2px]">
<a-checkbox-group v-model:model-value="statusFilters" direction="vertical" size="small">
<a-checkbox v-for="key of Object.keys(reviewStatusMap)" :key="key" :value="key">
<a-tag
:color="reviewStatusMap[key].color"
:class="[reviewStatusMap[key].class, 'px-[4px]']"
size="small"
>
{{ t(reviewStatusMap[key].label) }}
</a-tag>
</a-checkbox>
</a-checkbox-group>
</div>
</div>
</template>
</a-trigger>
</template>
<template #name="{ record }">
<a-button type="text" class="px-0">{{ record.name }}</a-button>
</template>
<template #status="{ record }">
<statusTag :status="record.status" />
</template>
<template #passRate="{ record }">
<div class="mr-[8px] w-[100px]">
<passRateLine :review-detail="record" height="5px" />
</div>
<div class="text-[var(--color-text-1)]">
{{ `${record.passRate || 0}%` }}
</div>
</template>
<template #operation="{ record }">
<MsButton>{{ t('testPlan.testPlanIndex.execution') }}</MsButton>
<a-divider direction="vertical" :margin="8"></a-divider>
<MsButton>{{ t('testPlan.testPlanIndex.copy') }}</MsButton>
<a-divider direction="vertical" :margin="8"></a-divider>
<MsTableMoreAction :list="moreActions" @select="handleMoreActionSelect($event, record)" />
</template>
</ms-base-table>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.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 MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import passRateLine from '@/views/case-management/caseReview/components/passRateLine.vue';
import statusTag from '@/views/case-management/caseReview/components/statusTag.vue';
import { reviewStatusMap } from '@/config/caseManagement';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore, useTableStore } from '@/store';
import { ModuleTreeNode } from '@/models/projectManagement/file';
import { ColumnEditTypeEnum, TableKeyEnum } from '@/enums/tableEnum';
const tableStore = useTableStore();
const { t } = useI18n();
const columns: MsTableColumn = [
{
title: 'testPlan.testPlanIndex.ID',
slotName: 'num',
dataIndex: 'num',
width: 200,
showInTable: true,
ellipsis: true,
showDrag: false,
},
{
title: 'testPlan.testPlanIndex.testPlanName',
slotName: 'name',
dataIndex: 'name',
showInTable: true,
showTooltip: true,
width: 300,
editType: ColumnEditTypeEnum.INPUT,
sortable: {
sortDirections: ['ascend', 'descend'],
},
ellipsis: true,
showDrag: false,
},
{
title: 'testPlan.testPlanIndex.desc',
slotName: 'desc',
showInTable: true,
width: 200,
showDrag: true,
},
{
title: 'testPlan.testPlanIndex.status',
dataIndex: 'status',
slotName: 'status',
titleSlotName: 'statusFilter',
width: 150,
},
{
title: 'testPlan.testPlanIndex.passRate',
dataIndex: 'passRate',
slotName: 'passRate',
showInTable: true,
width: 200,
showDrag: true,
},
{
title: 'testPlan.testPlanIndex.useCount',
slotName: 'versionId',
dataIndex: 'versionId',
width: 300,
showTooltip: true,
showInTable: true,
showDrag: true,
},
{
title: 'testPlan.testPlanIndex.bugCount',
slotName: 'moduleId',
dataIndex: 'moduleId',
showInTable: true,
width: 300,
showDrag: true,
},
{
title: 'testPlan.testPlanIndex.belongModule',
slotName: 'belongModule',
dataIndex: 'belongModule',
showInTable: true,
showDrag: true,
},
{
title: 'testPlan.testPlanIndex.creator',
slotName: 'createUser',
dataIndex: 'createUser',
showInTable: true,
width: 200,
showDrag: true,
},
{
title: 'testPlan.testPlanIndex.createTime',
slotName: 'createTime',
dataIndex: 'createTime',
showInTable: true,
sortable: {
sortDirections: ['ascend', 'descend'],
},
width: 200,
showDrag: true,
},
{
title: 'testPlan.testPlanIndex.operation',
slotName: 'operation',
dataIndex: 'operation',
fixed: 'right',
width: 260,
showInTable: true,
showDrag: false,
},
];
/**
* 更新测试计划名称
*/
async function updatePlanName() {
try {
Message.success(t('common.updateSuccess'));
return Promise.resolve(true);
} catch (error) {
console.log(error);
return Promise.resolve(false);
}
}
const keyword = ref<string>('');
const scrollWidth = ref<number>(3400);
const statusFilterVisible = ref(false);
const statusFilters = ref<string[]>(Object.keys(reviewStatusMap));
const tableBatchActions = {
baseAction: [
{
label: 'caseManagement.featureCase.export',
eventTag: 'export',
children: [
{
label: 'caseManagement.featureCase.exportExcel',
eventTag: 'exportExcel',
},
{
label: 'caseManagement.featureCase.exportXMind',
eventTag: 'exportXMind',
},
],
},
{
label: 'common.edit',
eventTag: 'batchEdit',
},
{
label: 'caseManagement.featureCase.moveTo',
eventTag: 'batchMoveTo',
},
{
label: 'caseManagement.featureCase.copyTo',
eventTag: 'batchCopyTo',
},
],
moreAction: [
{
label: 'caseManagement.featureCase.addDemand',
eventTag: 'addDemand',
},
{
label: 'caseManagement.featureCase.associatedDemand',
eventTag: 'associatedDemand',
},
{
label: 'caseManagement.featureCase.generatingDependencies',
eventTag: 'generatingDependencies',
},
{
label: 'caseManagement.featureCase.addToPublic',
eventTag: 'addToPublic',
},
{
isDivider: true,
},
{
label: 'common.delete',
eventTag: 'delete',
danger: true,
},
],
};
const moreActions: ActionsItem[] = [
{
label: 'common.delete',
danger: true,
eventTag: 'delete',
},
];
const tableSelected = ref<(string | number)[]>([]);
function handleTableSelect(selectArr: (string | number)[]) {
tableSelected.value = selectArr;
}
const data = [
{
id: '100944',
projectId: 'string',
num: '100944',
name: '系统示例',
status: 'PREPARED',
tags: ['string'],
schedule: 'string',
createUser: 'string',
createTime: 'string',
moduleName: 'string',
moduleId: 'string',
passCount: 0,
unPassCount: 0,
reviewedCount: 0,
underReviewedCount: 0,
children: [
{
id: '100945',
projectId: 'string',
num: '100945',
name: '系统示例',
status: 'COMPLETED',
tags: ['string'],
schedule: 'string',
createUser: 'string',
createTime: 'string',
moduleName: 'string',
moduleId: 'string',
testPlanItem: [],
testPlanGroupId: 'string',
passCount: 0,
unPassCount: 0,
reviewedCount: 0,
underReviewedCount: 0,
},
],
testPlanGroupId: 'string',
},
];
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, setKeyword, setAdvanceFilter, setProps } =
useTable(
undefined,
{
tableKey: TableKeyEnum.TEST_PLAN_ALL_TABLE,
scroll: { x: scrollWidth.value },
selectable: true,
showSetting: true,
heightUsed: 374,
enableDrag: true,
},
(item) => {
return {
...item,
tags: (item.tags || []).map((e: string) => ({ id: e, name: e })),
};
}
);
const showType = ref<string>('list');
const batchParams = ref<BatchActionQueryParams>({
selectedIds: [],
selectAll: false,
excludeIds: [],
currentSelectCount: 0,
});
function handleTableBatch(event: BatchActionParams, params: BatchActionQueryParams) {
batchParams.value = params;
}
function deletePlan(record: any) {}
function handleMoreActionSelect(item: ActionsItem, record: any) {
if (item.eventTag === 'delete') {
deletePlan(record);
}
}
tableStore.initColumn(TableKeyEnum.TEST_PLAN_ALL_TABLE, columns, 'drawer');
const expandedKeys = ref<string[]>([]);
function expandHandler(record: any) {
if (expandedKeys.value.includes(record.id)) {
expandedKeys.value = expandedKeys.value.filter((key) => key !== record.id);
} else {
expandedKeys.value = [...expandedKeys.value, record.id];
}
}
function getIconClass(record: any) {
return expandedKeys.value.includes(record.id) ? 'text-[rgb(var(--primary-5))]' : 'text-[var(--color-text-4)]';
}
function searchPlan() {}
function handleFilterHidden(val: boolean) {
if (!val) {
searchPlan();
}
}
onMounted(() => {
setProps({ data });
});
</script>
<style scoped lang="less">
:deep(.arco-table-cell-expand-icon .arco-table-cell-inline-icon) {
display: none;
}
:deep(.arco-table-cell-align-left) > span:first-child {
padding-left: 0 !important;
}
.arrowIcon {
transform: scaleX(-1);
}
</style>

View File

@ -0,0 +1,69 @@
<template>
<MsAdvanceFilter
:filter-config-list="filterConfigList"
:custom-fields-config-list="searchCustomFields"
:row-count="filterRowCount"
@keyword-search="fetchData"
@adv-search="handleAdvSearch"
>
<template #left>
<div class="flex w-full justify-between">
<div class="text-[var(--color-text-1)]"
>{{ moduleNamePath }}
<span class="text-[var(--color-text-4)]"> ({{ props.modulesCount[props.activeFolder] || 0 }})</span></div
>
<a-radio-group v-model="showType" type="button" class="file-show-type mr-2">
<a-radio value="all" class="show-type-icon p-[2px]">{{ t('testPlan.testPlanIndex.all') }}</a-radio>
<a-radio value="testPlan" class="show-type-icon p-[2px]">{{ t('testPlan.testPlanIndex.testPlan') }}</a-radio>
<a-radio value="testPlanGroup" class="show-type-icon p-[2px]">{{
t('testPlan.testPlanIndex.testPlanGroup')
}}</a-radio>
</a-radio-group>
</div>
</template>
</MsAdvanceFilter>
<AllTable v-if="showType === 'all'" />
<TestPlanTable v-if="showType === 'testPlan'" />
<TestPlanGroupTable v-if="showType === 'testPlanGroup'" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Message } from '@arco-design/web-vue';
import { MsAdvanceFilter } from '@/components/pure/ms-advance-filter';
import { FilterFormItem } from '@/components/pure/ms-advance-filter/type';
import AllTable from './allTable.vue';
import TestPlanGroupTable from './testplanGroup.vue';
import TestPlanTable from './testplanTable.vue';
import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n();
const props = defineProps<{
activeFolder: string;
activeFolderType: 'folder' | 'module';
offspringIds: string[]; // id
modulesCount: Record<string, number>; //
}>();
const emit = defineEmits<{
(e: 'init', params: any): void;
}>();
/** *
* 高级检索
*/
const filterConfigList = ref<FilterFormItem[]>([]);
const searchCustomFields = ref<FilterFormItem[]>([]);
const filterRowCount = ref(0);
const moduleNamePath = ref<string>('全部测试计划');
const showType = ref<string>('all');
function fetchData() {}
function handleAdvSearch() {}
</script>
<style scoped></style>

View File

@ -0,0 +1,358 @@
<template>
<a-input-search
v-model:model-value="groupKeyword"
:placeholder="t('caseManagement.featureCase.searchTip')"
allow-clear
class="mb-[16px]"
></a-input-search>
<a-spin class="w-full" :style="{ height: `calc(100vh - 346px)` }" :loading="loading">
<MsTree
v-model:focus-node-key="focusNodeKey"
:selected-keys="props.selectedKeys"
:data="caseTree"
:keyword="groupKeyword"
:node-more-actions="caseMoreActions"
:expand-all="props.isExpandAll"
:empty-text="t('testPlan.testPlanIndex.planEmptyContent')"
draggable
:virtual-list-props="virtualListProps"
block-node
:field-names="{
title: 'name',
key: 'id',
children: 'children',
count: 'count',
}"
title-tooltip-position="left"
@select="caseNodeSelect"
@more-action-select="handleCaseMoreSelect"
@more-actions-close="moreActionsClose"
@drop="handleDrag"
>
<template #title="nodeData">
<div class="inline-flex w-full">
<div class="one-line-text w-[calc(100%-32px)] text-[var(--color-text-1)]">{{ nodeData.name }}</div>
<div v-if="!props.isModal" class="ml-[4px] text-[var(--color-text-4)]">({{ nodeData.count || 0 }})</div>
</div>
</template>
<template v-if="!props.isModal" #extra="nodeData">
<MsPopConfirm
:visible="addSubVisible"
:is-delete="false"
:all-names="[]"
:title="t('testPlan.testPlanIndex.addSubModule')"
:ok-text="t('common.confirm')"
:field-config="{
placeholder: t('testPlan.testPlanIndex.addGroupTip'),
}"
:loading="confirmLoading"
@confirm="addSubModule"
@cancel="resetFocusNodeKey"
>
<MsButton type="icon" size="mini" class="ms-tree-node-extra__btn !mr-0" @click="setFocusKey(nodeData)">
<MsIcon type="icon-icon_add_outlined" size="14" class="text-[var(--color-text-4)]" />
</MsButton>
</MsPopConfirm>
<MsPopConfirm
:title="t('testPlan.testPlanIndex.rename')"
:all-names="[]"
:is-delete="false"
:ok-text="t('common.confirm')"
:field-config="{ field: renameCaseName }"
:loading="confirmLoading"
@confirm="updateNameModule"
@cancel="resetFocusNodeKey"
>
<span :id="`renameSpan${nodeData.id}`" class="relative"></span>
</MsPopConfirm>
</template>
</MsTree>
</a-spin>
</template>
<script setup lang="ts">
import { computed, onBeforeMount, ref, watch } from 'vue';
import { Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsPopConfirm from '@/components/pure/ms-popconfirm/index.vue';
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsTree from '@/components/business/ms-tree/index.vue';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import {
createPlanModuleTree,
getTestPlanModule,
moveTestPlanModuleTree,
updatePlanModuleTree,
} from '@/api/modules/test-plan/testPlan';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import useAppStore from '@/store/modules/app';
import { mapTree } from '@/utils';
import type { CreateOrUpdateModule, UpdateModule } from '@/models/caseManagement/featureCase';
import { ModuleTreeNode } from '@/models/projectManagement/file';
const { t } = useI18n();
const { openModal } = useModal();
const appStore = useAppStore();
const focusNodeKey = ref<string>('');
const loading = ref(false);
const props = defineProps<{
isModal?: boolean; //
activeFolder?: string; // 使
selectedKeys?: Array<string | number>; // key
isExpandAll: boolean; //
allNames?: string[]; // name
modulesCount?: Record<string, number>; //
}>();
const emits = defineEmits(['update:selectedKeys', 'planTreeNodeSelect', 'init']);
const currentProjectId = computed(() => appStore.currentProjectId);
const groupKeyword = ref<string>('');
const caseTree = ref<ModuleTreeNode[]>([]);
const setFocusKey = (node: MsTreeNodeData) => {
focusNodeKey.value = node.id || '';
};
const caseMoreActions: ActionsItem[] = [
{
label: 'caseManagement.featureCase.rename',
eventTag: 'rename',
},
{
label: 'caseManagement.featureCase.delete',
eventTag: 'delete',
danger: true,
},
];
const selectedNodeKeys = ref(props.selectedKeys || []);
watch(
() => props.selectedKeys,
(val) => {
selectedNodeKeys.value = val || [];
}
);
watch(
() => selectedNodeKeys.value,
(val) => {
emits('update:selectedKeys', val);
}
);
/**
* 初始化模块树
* @param isSetDefaultKey 是否设置第一个节点为选中节点
*/
async function initModules(isSetDefaultKey = false) {
try {
loading.value = true;
const res = await getTestPlanModule({ projectId: currentProjectId.value });
caseTree.value = mapTree<ModuleTreeNode>(res, (e) => {
return {
...e,
hideMoreAction: e.id === 'root',
draggable: e.id !== 'root' && !props.isModal,
disabled: e.id === props.activeFolder && props.isModal,
count: props.modulesCount?.[e.id] || 0,
};
});
if (isSetDefaultKey) {
selectedNodeKeys.value = [caseTree.value[0].id];
}
emits(
'init',
caseTree.value.map((e) => e.name)
);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
//
const deleteHandler = (node: MsTreeNodeData) => {
openModal({
type: 'error',
title: t('caseManagement.featureCase.deleteTipTitle', { name: node.name }),
content: t('caseManagement.featureCase.deleteCaseTipContent'),
okText: t('caseManagement.featureCase.deleteConfirm'),
okButtonProps: {
status: 'danger',
},
maskClosable: false,
onBeforeOk: async () => {
Message.success(t('caseManagement.featureCase.deleteSuccess'));
},
hideCancel: false,
});
};
const renamePopVisible = ref(false);
const renameCaseName = ref('');
function resetFocusNodeKey() {
focusNodeKey.value = '';
renamePopVisible.value = false;
renameCaseName.value = '';
}
//
const caseNodeSelect = (selectedKeys: (string | number)[], node: MsTreeNodeData) => {
const offspringIds: string[] = [];
mapTree(node.children || [], (e) => {
offspringIds.push(e.id);
return e;
});
emits('planTreeNodeSelect', selectedKeys, offspringIds);
};
//
const handleCaseMoreSelect = (item: ActionsItem, node: MsTreeNodeData) => {
switch (item.eventTag) {
case 'delete':
deleteHandler(node);
resetFocusNodeKey();
break;
case 'rename':
renameCaseName.value = node.name || '';
renamePopVisible.value = true;
document.querySelector(`#renameSpan${node.id}`)?.dispatchEvent(new Event('click'));
break;
default:
break;
}
};
/**
* 处理文件夹树节点拖拽事件
* @param tree 树数据
* @param dragNode 拖拽节点
* @param dropNode 释放节点
* @param dropPosition 释放位置
*/
async function handleDrag(
tree: MsTreeNodeData[],
dragNode: MsTreeNodeData,
dropNode: MsTreeNodeData,
dropPosition: number
) {
try {
loading.value = true;
await moveTestPlanModuleTree({
dragNodeId: dragNode.id as string,
dropNodeId: dropNode.id || '',
dropPosition,
});
Message.success(t('caseManagement.featureCase.moduleMoveSuccess'));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
initModules();
}
}
const moreActionsClose = () => {
if (!renamePopVisible.value) {
resetFocusNodeKey();
}
};
const addSubVisible = ref(false);
const confirmLoading = ref(false);
//
async function addSubModule(formValue?: { field: string }, cancel?: () => void) {
try {
confirmLoading.value = true;
const params: CreateOrUpdateModule = {
projectId: currentProjectId.value,
name: formValue?.field as string,
parentId: focusNodeKey.value,
};
await createPlanModuleTree(params);
Message.success(t('common.addSuccess'));
if (cancel) {
cancel();
}
initModules();
} catch (error) {
console.log(error);
} finally {
confirmLoading.value = false;
}
}
//
async function updateNameModule(formValue?: { field: string }, cancel?: () => void) {
try {
confirmLoading.value = true;
const params: UpdateModule = {
id: focusNodeKey.value,
name: formValue?.field as string,
};
await updatePlanModuleTree(params);
Message.success(t('common.updateSuccess'));
if (cancel) {
cancel();
}
initModules();
} catch (error) {
console.log(error);
} finally {
confirmLoading.value = false;
}
}
const virtualListProps = computed(() => {
return {
height: 'calc(100vh - 366px)',
};
});
watch(
() => props.activeFolder,
(val) => {
if (val === 'all') {
initModules();
}
}
);
/**
* 初始化模块文件数量
*/
watch(
() => props.modulesCount,
(obj) => {
caseTree.value = mapTree<ModuleTreeNode>(caseTree.value, (node) => {
return {
...node,
count: obj?.[node.id] || 0,
};
});
}
);
onBeforeMount(() => {
initModules();
});
defineExpose({
initModules,
});
</script>
<style scoped lang="less"></style>

View File

@ -0,0 +1,9 @@
<template>
<div>测试计划组 </div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
</script>
<style scoped></style>

View File

@ -0,0 +1,9 @@
<template>
<div>测试计划 </div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
</script>
<style scoped></style>

View File

@ -0,0 +1,231 @@
<template>
<div class="rounded-2xl bg-white">
<div class="p-[24px] pb-[16px]">
<a-button type="primary">
{{ t('testPlan.testPlanIndex.createTestPlan') }}
</a-button>
</div>
<a-divider class="!my-0" />
<div class="pageWrap">
<MsSplitBox>
<template #first>
<div class="p-[24px] pb-0">
<div class="test-plan h-[100%]">
<div class="case h-[38px]">
<div class="flex items-center" :class="getActiveClass('all')" @click="setActiveFolder('all')">
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
<div class="folder-name mx-[4px]">{{ t('testPlan.testPlanIndex.allTestPlan') }}</div>
<div class="folder-count">({{ modulesCount.all || 0 }})</div></div
>
<div class="ml-auto flex items-center">
<a-tooltip
:content="
isExpandAll ? t('testPlan.testPlanIndex.collapseAll') : t('testPlan.testPlanIndex.expandAll')
"
>
<MsButton type="icon" status="secondary" class="!mr-0 p-[4px]" @click="expandHandler">
<MsIcon :type="isExpandAll ? 'icon-icon_folder_collapse1' : 'icon-icon_folder_expansion1'" />
</MsButton>
</a-tooltip>
<MsPopConfirm
ref="confirmRef"
v-model:visible="addSubVisible"
:is-delete="false"
:title="t('testPlan.testPlanIndex.addSubModule')"
:all-names="rootModulesName"
:loading="confirmLoading"
:ok-text="t('common.confirm')"
:field-config="{
placeholder: t('testPlan.testPlanIndex.addGroupTip'),
}"
@confirm="confirmHandler"
>
<MsButton type="icon" class="!mr-0 p-[2px]">
<MsIcon
type="icon-icon_create_planarity"
size="18"
class="text-[rgb(var(--primary-5))] hover:text-[rgb(var(--primary-4))]"
/>
</MsButton>
</MsPopConfirm>
</div>
</div>
<a-divider class="my-[8px]" />
<TestPlanTree
ref="planTreeRef"
v-model:selected-keys="selectedKeys"
:all-names="rootModulesName"
:active-folder="activeFolder"
:is-expand-all="isExpandAll"
:modules-count="modulesCount"
@plan-tree-node-select="planNodeSelect"
@init="setRootModules"
></TestPlanTree>
</div>
</div>
</template>
<template #second>
<div class="p-[24px]">
<PlanTable
:active-folder="activeFolder"
:offspring-ids="offspringIds"
:active-folder-type="activeCaseType"
:modules-count="modulesCount"
@init="initModulesCount"
/>
</div>
</template>
</MsSplitBox>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
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 MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import PlanTable from './components/planTable.vue';
import TestPlanTree from './components/testPlanTree.vue';
import { createPlanModuleTree } from '@/api/modules/test-plan/testPlan';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import type { CaseModuleQueryParams, CreateOrUpdateModule, ValidateInfo } from '@/models/caseManagement/featureCase';
import Message from '@arco-design/web-vue/es/message';
const router = useRouter();
const appStore = useAppStore();
const { t } = useI18n();
const currentProjectId = computed(() => appStore.currentProjectId);
const activeFolder = ref<string>('all');
//
const getActiveClass = (type: string) => {
return activeFolder.value === type ? 'folder-text case-active' : 'folder-text';
};
const modulesCount = ref<Record<string, any>>({});
//
const selectedKeys = computed({
get: () => [activeFolder.value],
set: (val) => val,
});
const isExpandAll = ref(false);
//
const expandHandler = () => {
isExpandAll.value = !isExpandAll.value;
};
const addSubVisible = ref(false);
const rootModulesName = ref<string[]>([]);
const planTreeRef = ref();
const confirmLoading = ref(false);
const confirmRef = ref();
async function confirmHandler() {
try {
confirmLoading.value = true;
const { field } = confirmRef.value.form;
if (!confirmRef.value.isPass) {
return;
}
const params: CreateOrUpdateModule = {
projectId: currentProjectId.value,
name: field,
parentId: 'NONE',
};
await createPlanModuleTree(params);
Message.success(t('caseManagement.featureCase.addSuccess'));
planTreeRef.value.initModules();
addSubVisible.value = false;
} catch (error) {
console.log(error);
} finally {
confirmLoading.value = false;
}
}
// ||
const setActiveFolder = (type: string) => {
activeFolder.value = type;
};
const activeCaseType = ref<'folder' | 'module'>('folder'); //
const offspringIds = ref<string[]>([]);
//
function planNodeSelect(keys: string[], _offspringIds: string[]) {
[activeFolder.value] = keys;
activeCaseType.value = 'module';
offspringIds.value = [..._offspringIds];
}
/**
* 设置根模块名称列表
* @param names 根模块名称列表
*/
function setRootModules(names: string[]) {
rootModulesName.value = names;
}
/**
* 右侧表格数据刷新后若当前展示的是模块则刷新模块树的统计数量
*/
function initModulesCount(params: any) {}
</script>
<style scoped>
.pageWrap {
min-width: 1000px;
height: calc(100vh - 166px);
border-radius: var(--border-radius-large);
@apply bg-white;
.case {
padding: 8px 4px;
border-radius: var(--border-radius-small);
@apply flex cursor-pointer items-center justify-between;
&:hover {
background-color: rgb(var(--primary-1));
}
.folder-icon {
margin-right: 4px;
color: var(--color-text-4);
}
.folder-name {
color: var(--color-text-1);
}
.folder-count {
margin-left: 4px;
color: var(--color-text-4);
}
.case-active {
.folder-icon,
.folder-name,
.folder-count {
color: rgb(var(--primary-5));
}
}
.back {
margin-right: 8px;
width: 20px;
height: 20px;
border: 1px solid #ffffff;
background: linear-gradient(90deg, rgb(var(--primary-9)) 3.36%, #ffffff 100%);
box-shadow: 0 0 7px rgb(15 0 78 / 9%);
.arco-icon {
color: rgb(var(--primary-5));
}
@apply flex cursor-pointer items-center rounded-full;
}
}
}
</style>

View File

@ -0,0 +1,24 @@
export default {
'testPlan.testPlanIndex.createTestPlan': 'create test plan',
'testPlan.testPlanIndex.allTestPlan': 'All test Plans',
'testPlan.testPlanIndex.collapseAll': 'Collapse all submodules',
'testPlan.testPlanIndex.expandAll': 'Expand all submodules',
'testPlan.testPlanIndex.addSubModule': 'Add submodule',
'testPlan.testPlanIndex.addGroupTip': 'Please enter the group name, press enter to save',
'testPlan.testPlanIndex.planEmptyContent': 'No test plan data, please tap the button above to create or import',
'testPlan.testPlanIndex.rename': 'rename',
'testPlan.testPlanIndex.all': 'All',
'testPlan.testPlanIndex.testPlan': 'Test plan',
'testPlan.testPlanIndex.testPlanGroup': 'Test planning groups',
'testPlan.testPlanIndex.testPlanName': 'name',
'testPlan.testPlanIndex.ID': 'ID',
'testPlan.testPlanIndex.desc': 'Description',
'testPlan.testPlanIndex.status': 'Execution state',
'testPlan.testPlanIndex.passRate': 'Pass Rate',
'testPlan.testPlanIndex.useCount': 'Use cases',
'testPlan.testPlanIndex.bugCount': 'bug count',
'testPlan.testPlanIndex.belongModule': 'belong module',
'testPlan.testPlanIndex.creator': 'creator',
'testPlan.testPlanIndex.createTime': 'create time',
'testPlan.testPlanIndex.operation': 'operation',
};

View File

@ -0,0 +1,26 @@
export default {
'testPlan.testPlanIndex.createTestPlan': '创建测试计划',
'testPlan.testPlanIndex.allTestPlan': '全部测试计划',
'testPlan.testPlanIndex.collapseAll': '收起全部子模块',
'testPlan.testPlanIndex.expandAll': '展开全部子模块',
'testPlan.testPlanIndex.addSubModule': '添加子模块',
'testPlan.testPlanIndex.addGroupTip': '请输入分组名称,按回车键保存',
'testPlan.testPlanIndex.planEmptyContent': '暂无测试计划数据,请点击上方按钮创建或导入',
'testPlan.testPlanIndex.rename': '重命名',
'testPlan.testPlanIndex.all': '全部',
'testPlan.testPlanIndex.testPlan': '测试计划',
'testPlan.testPlanIndex.testPlanGroup': '测试计划组',
'testPlan.testPlanIndex.testPlanName': '测试计划名称',
'testPlan.testPlanIndex.ID': 'ID',
'testPlan.testPlanIndex.desc': '描述',
'testPlan.testPlanIndex.status': '执行状态',
'testPlan.testPlanIndex.passRate': '通过率',
'testPlan.testPlanIndex.useCount': '用例数',
'testPlan.testPlanIndex.bugCount': 'bug数',
'testPlan.testPlanIndex.belongModule': '所属模块',
'testPlan.testPlanIndex.creator': '创建人',
'testPlan.testPlanIndex.createTime': '创建时间',
'testPlan.testPlanIndex.operation': '操作',
'testPlan.testPlanIndex.execution': '执行',
'testPlan.testPlanIndex.copy': '复制',
};