feat(功能用例&测试计划): 功能用例附件联调和bug修改&测试计划模块展开行页面
This commit is contained in:
parent
d96e9d2795
commit
ab87dcca8a
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
|
@ -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)',
|
||||
},
|
||||
|
|
|
@ -1 +1 @@
|
|||
export type CaseLevel = 'P1' | 'P2' | 'P3' | 'P4';
|
||||
export type CaseLevel = 'P0' | 'P1' | 'P2' | 'P3';
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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' },
|
||||
|
|
|
@ -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 '';
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
@ -50,6 +50,7 @@ export enum ProjectManagementRouteEnum {
|
|||
|
||||
export enum TestPlanRouteEnum {
|
||||
TEST_PLAN = 'testPlan',
|
||||
TEST_PLAN_INDEX = 'testPlanIndex',
|
||||
}
|
||||
|
||||
export enum UITestRouteEnum {
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
|
||||
// 具有特殊功能的列
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export default {};
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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 需要转换的字符串
|
||||
|
|
|
@ -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[],
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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'), // 总文件列表
|
||||
};
|
||||
|
|
|
@ -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 {};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -66,6 +66,8 @@ export default {
|
|||
paramEnvironmentSetGlobalVariable: '设置环境参数',
|
||||
insertPublicScript: '插入公共脚本',
|
||||
terminationTest: '终止测试',
|
||||
code_hide_report_length: '隐藏报文长度',
|
||||
code_add_report_length: '报文头添加长度',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
<template> TestPlan is waiting for development </template>
|
||||
|
||||
<script setup></script>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<div>测试计划组 </div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<div>测试计划 </div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -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>
|
|
@ -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',
|
||||
};
|
|
@ -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': '复制',
|
||||
};
|
Loading…
Reference in New Issue