feat(脑图): 标签&优先级&添加交互规则

This commit is contained in:
baiqi 2024-05-14 18:04:33 +08:00 committed by Craftsman
parent 318b7fc950
commit 3209249045
22 changed files with 819 additions and 271 deletions

View File

@ -467,7 +467,7 @@
.arco-radio-button {
@apply bg-transparent;
margin: 1px;
line-height: 20px;
}
.arco-radio-checked {
@apply bg-white;

View File

@ -0,0 +1,313 @@
<template>
<MsMinderEditor
:tags="tags"
:import-json="props.importJson"
:replaceable-tags="replaceableTags"
:insert-node="insertNode"
:priority-disable-check="priorityDisableCheck"
:after-tag-edit="afterTagEdit"
single-tag
tag-enable
sequence-enable
@click="handleNodeClick"
>
</MsMinderEditor>
</template>
<script setup lang="ts">
import MsMinderEditor from '@/components/pure/ms-minder-editor/minderEditor.vue';
import type { MinderJson, MinderJsonNode, MinderJsonNodeData } from '@/components/pure/ms-minder-editor/props';
import { useI18n } from '@/hooks/useI18n';
import { getGenerateId } from '@/utils';
const props = defineProps<{
importJson: MinderJson;
}>();
const { t } = useI18n();
const caseTag = t('common.case');
const moduleTag = t('common.module');
const topTags = [moduleTag, caseTag];
const descTags = [t('ms.minders.stepDesc'), t('ms.minders.textDesc')];
const tags = [...topTags, t('ms.minders.precondition'), ...descTags, t('ms.minders.stepExpect'), t('common.remark')];
const visible = ref<boolean>(false);
const nodeData = ref<any>({});
function handleNodeClick(data: any) {
if (data.resource && data.resource.includes(caseTag)) {
visible.value = true;
nodeData.value = data;
}
}
/**
* 已选中节点的可替换标签判断
* @param node 选中节点
*/
function replaceableTags(node: MinderJsonNode) {
if (node.data?.resource?.some((e) => topTags.includes(e))) {
//
return !node.children || node.children.length === 0
? topTags.filter((tag) => !node.data?.resource?.includes(tag))
: [];
}
if (node.data?.resource?.some((e) => descTags.includes(e))) {
//
return descTags.filter((tag) => !node.data?.resource?.includes(tag));
}
if (
(!node.data?.resource || node.data?.resource?.length === 0) &&
(!node.parent?.data?.resource ||
node.parent?.data?.resource.length === 0 ||
node.parent?.data?.resource?.some((e) => topTags.includes(e)))
) {
//
//
return node.children &&
(node.children.some((e) => e.data?.resource?.includes(caseTag)) ||
node.children.some((e) => e.data?.resource?.includes(moduleTag)))
? topTags.filter((e) => e !== caseTag)
: topTags;
}
return [];
}
function execInert(command: string, node?: MinderJsonNodeData) {
if (window.minder.queryCommandState(command) !== -1) {
window.minder.execCommand(command, node);
}
}
/**
* 插入前置条件
* @param node 目标节点
* @param type 插入类型
*/
function inertPrecondition(node: MinderJsonNode, type: string) {
const child: MinderJsonNode = {
parent: node,
data: {
id: getGenerateId(),
text: t('ms.minders.precondition'),
resource: [t('ms.minders.precondition')],
expandState: 'expand',
},
children: [],
};
const sibling = {
parent: child,
data: {
id: getGenerateId(),
text: '',
resource: [],
},
};
execInert(type, child.data);
nextTick(() => {
execInert('AppendChildNode', sibling.data);
});
}
/**
* 插入备注
* @param node 目标节点
* @param type 插入类型
*/
function insetRemark(node: MinderJsonNode, type: string) {
const child = {
parent: node,
data: {
id: getGenerateId(),
text: t('common.remark'),
resource: [t('common.remark')],
},
children: [],
};
execInert(type, child.data);
}
// function insertTextDesc(node: MinderJsonNode, type: string) {
// const child = {
// parent: node,
// data: {
// id: getGenerateId(),
// text: t('ms.minders.textDesc'),
// resource: [t('ms.minders.textDesc')],
// },
// children: [],
// };
// const sibling = {
// parent: child,
// data: {
// id: getGenerateId(),
// text: t('ms.minders.stepExpect'),
// resource: [t('ms.minders.stepExpect')],
// },
// };
// execInert(type, {
// ...child,
// children: [sibling],
// });
// }
/**
* 插入步骤描述
* @param node 目标节点
* @param type 插入类型
*/
function insetStepDesc(node: MinderJsonNode, type: string) {
const child = {
parent: node,
data: {
id: getGenerateId(),
text: t('ms.minders.stepDesc'),
resource: [t('ms.minders.stepDesc')],
},
children: [],
};
const sibling = {
parent: child,
data: {
id: getGenerateId(),
text: t('ms.minders.stepExpect'),
resource: [t('ms.minders.stepExpect')],
},
};
execInert(type, child.data);
nextTick(() => {
execInert('AppendChildNode', sibling.data);
});
}
/**
* 插入预期结果
* @param node 目标节点
* @param type 插入类型
*/
function insertExpect(node: MinderJsonNode, type: string) {
const child = {
parent: node,
data: {
id: getGenerateId(),
text: t('ms.minders.stepExpect'),
resource: [t('ms.minders.stepExpect')],
},
children: [],
};
execInert(type, child.data);
}
/**
* 插入节点
* @param node 目标节点
* @param type 插入类型
*/
function insertNode(node: MinderJsonNode, type: string) {
switch (type) {
case 'AppendChildNode':
if (node.data?.resource?.includes(moduleTag)) {
execInert('AppendChildNode');
} else if (node.data?.resource?.includes(caseTag)) {
//
if (!node.children || node.children.length === 0) {
//
inertPrecondition(node, type);
} else if (node.children.length > 0) {
//
let hasPreCondition = false;
let hasTextDesc = false;
let hasRemark = false;
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
if (child.data?.resource?.includes(t('ms.minders.precondition'))) {
hasPreCondition = true;
} else if (child.data?.resource?.includes(t('ms.minders.textDesc'))) {
hasTextDesc = true;
} else if (child.data?.resource?.includes(t('common.remark'))) {
hasRemark = true;
}
}
if (!hasPreCondition) {
//
inertPrecondition(node, type);
} else if (!hasRemark) {
//
insetRemark(node, type);
} else if (!hasTextDesc) {
//
insetStepDesc(node, type);
}
}
} else if (
(node.data?.resource?.includes(t('ms.minders.stepDesc')) ||
node.data?.resource?.includes(t('ms.minders.textDesc'))) &&
(!node.children || node.children.length === 0)
) {
//
insertExpect(node, 'AppendChildNode');
} else if (node.data?.resource?.includes(t('ms.minders.precondition'))) {
//
execInert('AppendChildNode');
}
break;
case 'AppendParentNode':
execInert('AppendParentNode');
break;
case 'AppendSiblingNode':
if (node.parent?.data?.resource?.includes(caseTag) && node.parent?.children) {
//
let hasPreCondition = false;
let hasTextDesc = false;
let hasRemark = false;
for (let i = 0; i < node.parent.children.length; i++) {
const sibling = node.parent.children[i];
if (sibling.data?.resource?.includes(t('ms.minders.precondition'))) {
hasPreCondition = true;
} else if (sibling.data?.resource?.includes(t('common.remark'))) {
hasRemark = true;
} else if (sibling.data?.resource?.includes(t('ms.minders.textDesc'))) {
hasTextDesc = true;
}
}
if (!hasPreCondition) {
//
inertPrecondition(node, type);
} else if (!hasRemark) {
//
insetRemark(node, type);
} else if (!hasTextDesc) {
//
insetStepDesc(node, type);
}
} else if (node.parent?.data?.resource?.includes(moduleTag) || !node.parent?.data?.resource) {
//
execInert('AppendSiblingNode');
}
break;
default:
break;
}
}
function priorityDisableCheck(node: MinderJsonNode) {
if (node.data?.resource?.includes(caseTag)) {
return false;
}
return true;
}
/**
* 标签编辑后如果将标签修改为模块则删除已添加的优先级
* @param node 选中节点
* @param tag 更改后的标签
*/
function afterTagEdit(node: MinderJsonNode, tag: string) {
if (tag === moduleTag && node.data) {
window.minder.execCommand('priority');
}
}
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,15 @@
<template>
<FeatureCaseMinder :import-json="props.importJson" />
</template>
<script setup lang="ts">
import type { MinderJson } from '@/components/pure/ms-minder-editor/props';
import FeatureCaseMinder from '@/components/business/ms-minders/featureCaseMinder.vue';
const props = defineProps<{
minderType: 'FeatureCase';
importJson: MinderJson;
}>();
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,6 @@
export default {
'ms.minders.precondition': 'Precondition',
'ms.minders.stepDesc': 'Step Description',
'ms.minders.stepExpect': 'Expected Result',
'ms.minders.textDesc': 'Text Description',
};

View File

@ -0,0 +1,6 @@
export default {
'ms.minders.precondition': '前置条件',
'ms.minders.stepDesc': '步骤描述',
'ms.minders.stepExpect': '预期结果',
'ms.minders.textDesc': '文本描述',
};

View File

@ -1,7 +1,7 @@
<template>
<div class="flex flex-row items-center justify-between">
<slot name="left"></slot>
<div class="flex flex-row gap-[8px]">
<div class="flex flex-row gap-[12px]">
<a-input-search
v-model:modelValue="innerKeyword"
size="small"

View File

@ -1,34 +1,55 @@
<template>
<header>
<div class="mind-tab-panel">
<edit-menu
:minder="minder"
:move-enable="props.moveEnable"
:move-confirm="props.moveConfirm"
:sequence-enable="props.sequenceEnable"
:tag-enable="props.tagEnable"
:progress-enable="props.progressEnable"
:priority-count="props.priorityCount"
:priority-prefix="props.priorityPrefix"
:tag-edit-check="props.tagEditCheck"
:tag-disable-check="props.tagDisableCheck"
:priority-disable-check="props.priorityDisableCheck"
:priority-start-with-zero="props.priorityStartWithZero"
:tags="props.tags"
:distinct-tags="props.distinctTags"
:del-confirm="props.delConfirm"
/>
</div>
</header>
<!-- <div class="ms-minder-editor-header">
<a-tooltip v-for="item of props.iconButtons" :key="item.eventTag" :content="t(item.tooltip)">
<MsButton type="icon" class="ms-minder-editor-header-icon-button" @click="emit('click', item.eventTag)">
<MsIcon :type="item.icon" class="text-[var(--color-text-4)]" />
</MsButton>
</a-tooltip>
<a-divider v-if="props.iconButtons?.length" direction="vertical" :margin="8"></a-divider>
<a-tooltip :content="isFullScreen ? t('common.offFullScreen') : t('common.fullScreen')">
<MsButton v-if="isFullScreen" type="icon" class="ms-minder-editor-header-icon-button" @click="toggleFullScreen">
<MsIcon type="icon-icon_off_screen" class="text-[var(--color-text-4)]" />
</MsButton>
<MsButton v-else type="icon" class="ms-minder-editor-header-icon-button" @click="toggleFullScreen">
<MsIcon type="icon-icon_full_screen_one" class="text-[var(--color-text-4)]" />
</MsButton>
</a-tooltip>
</div> -->
<div class="mind-tab-panel">
<editMenu
:minder="minder"
:move-enable="props.moveEnable"
:move-confirm="props.moveConfirm"
:sequence-enable="props.sequenceEnable"
:tag-enable="props.tagEnable"
:progress-enable="props.progressEnable"
:priority-count="props.priorityCount"
:priority-prefix="props.priorityPrefix"
:tag-edit-check="props.tagEditCheck"
:tag-disable-check="props.tagDisableCheck"
:priority-disable-check="props.priorityDisableCheck"
:priority-start-with-zero="props.priorityStartWithZero"
:tags="props.tags"
:distinct-tags="props.distinctTags"
:del-confirm="props.delConfirm"
:replaceable-tags="props.replaceableTags"
:single-tag="props.singleTag"
:insert-node="props.insertNode"
:after-tag-edit="props.afterTagEdit"
/>
</div>
</template>
<script lang="ts" name="headerVue" setup>
<script lang="ts" setup>
// import MsButton from '@/components/pure/ms-button/index.vue';
// import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import editMenu from '../menu/edit/editMenu.vue';
import { delProps, editMenuProps, moleProps, priorityProps, tagProps, viewMenuProps } from '../props';
import { delProps, editMenuProps, insertProps, moleProps, priorityProps, tagProps, viewMenuProps } from '../props';
const props = defineProps({
...editMenuProps,
...insertProps,
...moleProps,
...priorityProps,
...tagProps,
@ -36,6 +57,21 @@
...viewMenuProps,
minder: null,
});
// import useFullScreen from '@/hooks/useFullScreen';
// import { useI18n } from '@/hooks/useI18n';
// import { headerProps } from '../props';
// const props = defineProps({
// ...headerProps,
// });
// const emit = defineEmits<{
// (e: 'click', eventTag: string): void;
// }>();
// const { t } = useI18n();
// const { toggleFullScreen, isFullScreen } = useFullScreen(document.querySelector('.ms-minder-editor-container'));
</script>
<style lang="less">
@ -46,4 +82,21 @@
background-repeat: no-repeat;
}
}
// .ms-minder-editor-header {
// @apply absolute z-10 flex items-center bg-white;
// top: 24px;
// right: 0;
// padding: 4px 8px;
// border-radius: var(--border-radius-small);
// box-shadow: 0 4px 10px -1px rgb(100 100 102 / 15%);
// .ms-minder-editor-header-icon-button {
// &:hover {
// background-color: rgb(var(--primary-1)) !important;
// .arco-icon {
// color: rgb(var(--primary-4)) !important;
// }
// }
// }
// }
</style>

View File

@ -1,5 +1,5 @@
<template>
<div ref="mec" class="minder-container" :style="{ height: `${props.height}px` }">
<div ref="mec" class="minder-container">
<a-button type="primary" :disabled="props.disabled" class="save-btn bottom-[30px] right-[30px]" @click="save">
{{ t('minder.main.main.save') }}
</a-button>
@ -92,13 +92,13 @@
import useMinderStore from '@/store/modules/components/minder-editor';
import { findNodePathByKey } from '@/utils';
import { editMenuProps, mainEditorProps, priorityProps, tagProps } from '../props';
import { editMenuProps, insertProps, mainEditorProps, MinderJsonNode, priorityProps, tagProps } from '../props';
import Editor from '../script/editor';
import { markChangeNode, markDeleteNode } from '../script/tool/utils';
import type { Ref } from 'vue';
const { t } = useI18n();
const props = defineProps({ ...editMenuProps, ...mainEditorProps, ...tagProps, ...priorityProps });
const props = defineProps({ ...editMenuProps, ...insertProps, ...mainEditorProps, ...tagProps, ...priorityProps });
const emit = defineEmits({
afterMount: () => ({}),
@ -246,6 +246,17 @@
}
);
function execInsertCommand(command: string) {
const node: MinderJsonNode = window.minder.getSelectedNode();
if (props.insertNode) {
props.insertNode(node, command);
return;
}
if (window.minder.queryCommandState(command) !== -1) {
window.minder.execCommand(command);
}
}
function handleMinderMenuSelect(val: string | number | Record<string, any> | undefined) {
const selectedNode = window.minder.getSelectedNode();
switch (val) {
@ -257,13 +268,13 @@
}
break;
case 'insetParent':
window.minder.execCommand('AppendParentNode');
execInsertCommand('AppendParentNode');
break;
case 'insetSon':
window.minder.execCommand('AppendChildNode');
execInsertCommand('AppendChildNode');
break;
case 'insetBrother':
window.minder.execCommand('AppendSiblingNode');
execInsertCommand('AppendSiblingNode');
break;
case 'copy':
window.minder.execCommand('Copy');
@ -292,7 +303,9 @@
@apply !absolute;
}
.minder-container {
@apply relative;
@apply relative !bg-white;
height: calc(100% - 60px);
}
.minder-dropdown {
.arco-dropdown-list-wrapper {

View File

@ -18,18 +18,19 @@
{{ t('minder.menu.expand.folding') }}
</div>
<move-box :move-enable="props.moveEnable" :move-confirm="props.moveConfirm" />
<insert-box />
<insert-box :insert-node="props.insertNode" />
<edit-del :del-confirm="props.delConfirm" />
</div>
<div class="menu-group">
<tag-box
v-if="props.tagEnable"
:tags="props.tags"
:tag-disable-check="props.tagDisableCheck"
:tag-edit-check="props.tagEditCheck"
:distinct-tags="props.distinctTags"
/>
</div>
<tag-box
v-if="props.tagEnable"
:tags="props.tags"
:tag-disable-check="props.tagDisableCheck"
:tag-edit-check="props.tagEditCheck"
:distinct-tags="props.distinctTags"
:replaceable-tags="props.replaceableTags"
:single-tag="props.singleTag"
:after-tag-edit="props.afterTagEdit"
/>
<div class="menu-group">
<sequence-box
v-if="props.sequenceEnable"
@ -55,10 +56,11 @@
import { useI18n } from '@/hooks/useI18n';
import { delProps, editMenuProps, moleProps, priorityProps, tagProps, viewMenuProps } from '../../props';
import { delProps, editMenuProps, insertProps, moleProps, priorityProps, tagProps, viewMenuProps } from '../../props';
const props = defineProps({
...editMenuProps,
...insertProps,
...priorityProps,
...tagProps,
...delProps,

View File

@ -26,8 +26,13 @@
import { useI18n } from '@/hooks/useI18n';
import { insertProps, MinderJsonNode } from '../../props';
import { isDisableNode } from '../../script/tool/utils';
const props = defineProps({
...insertProps,
});
const { t } = useI18n();
const minder = ref<any>({});
@ -58,6 +63,11 @@
});
function execCommand(command: string) {
const node: MinderJsonNode = minder.value.getSelectedNode();
if (props.insertNode) {
props.insertNode(node, command);
return;
}
if (minder.value.queryCommandState(command) !== -1) {
minder.value.execCommand(command);
}

View File

@ -53,7 +53,7 @@
return true;
}
if (props.priorityDisableCheck) {
return props.priorityDisableCheck();
return props.priorityDisableCheck(node);
}
return !!minder.queryCommandState && minder.queryCommandState('priority') === -1;
};

View File

@ -1,7 +1,7 @@
<template>
<div class="flex items-center">
<div v-if="tagList.length > 0" class="menu-group flex items-center">
<a-tag
v-for="item in props.tags"
v-for="item in tagList"
:key="item"
:color="getResourceColor(item)"
:class="commandDisabled ? 'disabledTag' : ''"
@ -15,7 +15,7 @@
<script lang="ts" name="TagBox" setup>
import { nextTick, onMounted, reactive, ref } from 'vue';
import { tagProps } from '../../props';
import { MinderJsonNode, tagProps } from '../../props';
import { isDisableNode, isTagEnable } from '../../script/tool/utils';
const props = defineProps(tagProps);
@ -34,11 +34,21 @@
return !!minder.queryCommandState && minder.queryCommandState('resource') === -1;
};
const tagList = ref(props.tags);
onMounted(() => {
nextTick(() => {
minder = window.minder;
minder.on('selectionchange', () => {
commandDisabled.value = isDisable();
const node: MinderJsonNode = minder.getSelectedNode();
if (commandDisabled.value) {
tagList.value = [];
} else if (props.replaceableTags) {
tagList.value = props.replaceableTags(node);
} else {
tagList.value = [];
}
});
});
});
@ -54,7 +64,8 @@
return;
}
if (props.tagEditCheck) {
if (!props.tagEditCheck(resourceName)) {
const node: MinderJsonNode = minder.getSelectedNode();
if (!props.tagEditCheck(node, resourceName)) {
return;
}
}
@ -62,22 +73,33 @@
return;
}
const origin = window.minder.queryCommandValue('resource');
const index = origin.indexOf(resourceName);
//
if (props.distinctTags.indexOf(resourceName) > -1) {
for (let i = 0; i < origin.length; i++) {
if (props.distinctTags.indexOf(origin[i]) > -1) {
origin.splice(i, 1);
i--;
if (props.singleTag) {
origin.splice(0, origin.length, resourceName);
} else {
const index = origin.indexOf(resourceName);
//
if (props.distinctTags.indexOf(resourceName) > -1) {
for (let i = 0; i < origin.length; i++) {
if (props.distinctTags.indexOf(origin[i]) > -1) {
origin.splice(i, 1);
i--;
}
}
}
}
if (index !== -1) {
origin.splice(index, 1);
} else {
origin.push(resourceName);
if (index !== -1) {
origin.splice(index, 1);
} else {
origin.push(resourceName);
}
}
window.minder.execCommand('resource', origin);
const node: MinderJsonNode = minder.getSelectedNode();
if (props.replaceableTags) {
tagList.value = props.replaceableTags(node);
}
if (props.afterTagEdit) {
props.afterTagEdit(node, resourceName);
}
}
</script>

View File

@ -1,6 +1,6 @@
<template>
<div class="main-container">
<header-menu
<div class="ms-minder-editor-container">
<minderHeader
:sequence-enable="props.sequenceEnable"
:tag-enable="props.tagEnable"
:progress-enable="props.progressEnable"
@ -20,9 +20,13 @@
:mold-enable="props.moldEnable"
:font-enable="props.fontEnable"
:style-enable="props.styleEnable"
:replaceable-tags="props.replaceableTags"
:single-tag="props.singleTag"
:insert-node="props.insertNode"
:after-tag-edit="props.afterTagEdit"
@mold-change="handleMoldChange"
/>
<main-editor
<mainEditor
:disabled="props.disabled"
:sequence-enable="props.sequenceEnable"
:tag-enable="props.tagEnable"
@ -38,6 +42,7 @@
:priority-count="props.priorityCount"
:priority-prefix="props.priorityPrefix"
:priority-start-with-zero="props.priorityStartWithZero"
:insert-node="props.insertNode"
@after-mount="emit('afterMount')"
@save="save"
@enter-node="handleEnterNode"
@ -48,10 +53,20 @@
<script lang="ts" name="minderEditor" setup>
import { onMounted } from 'vue';
import headerMenu from './main/header.vue';
import minderHeader from './main/header.vue';
import mainEditor from './main/mainEditor.vue';
import { delProps, editMenuProps, mainEditorProps, moleProps, priorityProps, tagProps, viewMenuProps } from './props';
import {
delProps,
editMenuProps,
headerProps,
insertProps,
mainEditorProps,
moleProps,
priorityProps,
tagProps,
viewMenuProps,
} from './props';
const emit = defineEmits<{
(e: 'moldChange', data: number): void;
@ -62,6 +77,8 @@
}>();
const props = defineProps({
...headerProps,
...insertProps,
...editMenuProps,
...mainEditorProps,
...moleProps,
@ -103,3 +120,9 @@
});
});
</script>
<style lang="less" scoped>
.ms-minder-editor-container {
@apply relative h-full;
}
</style>

View File

@ -2,9 +2,35 @@
* Api
*/
import type { PropType } from 'vue';
export interface MinderIconButtonItem {
icon: string;
tooltip: string;
eventTag: string;
}
export interface MinderJsonNodeData {
id: string;
text: string;
resource?: string[];
expandState?: string;
priority?: number;
}
export interface MinderJsonNode {
parent?: MinderJsonNode;
data?: MinderJsonNodeData;
children?: MinderJsonNode[];
}
export interface MinderJson {
root: MinderJsonNode;
template: string;
treePath: Record<string, MinderJsonNode>[];
}
export const mainEditorProps = {
importJson: {
type: Object,
type: Object as PropType<MinderJson>,
default() {
return {
root: {},
@ -20,6 +46,12 @@ export const mainEditorProps = {
disabled: Boolean,
};
export const headerProps = {
iconButtons: {
type: [] as PropType<MinderIconButtonItem[]>,
},
};
export const priorityProps = {
priorityCount: {
type: Number,
@ -39,27 +71,45 @@ export const priorityProps = {
type: String,
default: 'P',
},
priorityDisableCheck: Function,
priorityDisableCheck: Function as PropType<(node: MinderJsonNode) => boolean>,
operators: [],
};
export interface MinderReplaceTag {
tags: string[];
condition: (node: MinderJsonNode, tags: string[]) => boolean;
}
export const tagProps = {
tags: {
// 自定义标签
type: Array<string>,
default() {
return [] as string[];
return [];
},
},
distinctTags: {
// 个别标签二选一
type: Array<string>,
default() {
return [] as string[];
return [];
},
},
singleTag: {
// 单标签
type: Boolean,
default: false,
},
replaceableTags: Function as PropType<(node: MinderJsonNode) => string[]>,
tagDisableCheck: Function,
tagEditCheck: Function,
tagEditCheck: Function as PropType<(node: MinderJsonNode, tag: string) => boolean>,
afterTagEdit: Function as PropType<(node: MinderJsonNode, tag: string) => void>,
};
export const insertProps = {
insertNode: {
type: Function as PropType<(node: MinderJsonNode, type: string) => void>,
default: undefined,
},
};
export const editMenuProps = {

View File

@ -1,6 +1,6 @@
@import 'dropdown-list.less';
.mind-tab-panel {
@apply h-full w-full;
@apply w-full;
.menu-container {
@apply flex;

View File

@ -754,6 +754,9 @@
.arco-table-td-content {
@apply justify-center;
}
.arco-table-cell {
padding: 0;
}
}
:deep(.ms-table-select-all) {
.dropdown-icon {

View File

@ -173,4 +173,6 @@ export default {
'common.fakeError': 'Fake error',
'common.belongModule': 'Belong module',
'common.moreSetting': 'More settings',
'common.remark': 'Remark',
'common.case': 'Case',
};

View File

@ -176,4 +176,6 @@ export default {
'common.executionResult': '执行结果',
'common.detail': '详情',
'common.baseInfo': '基本信息',
'common.remark': '备注',
'common.case': '用例',
};

View File

@ -252,7 +252,7 @@
JSONPath({
json: parseJson.value,
path: expressionForm.value.expression,
})?.map((e: any) => JSON.stringify(e).replace(/"Number\(([^)]+)\)"|Number\(([^)]+)\)/g, '$1$2')) || [];
})?.map((e: any) => JSON.stringify(e).replace(/Number\(([^)]+)\)|Number\(([^)]+)\)/g, '$1$2')) || [];
} catch (error) {
matchResult.value = JSONPath({ json: props.response || '', path: expressionForm.value.expression }) || [];
}

View File

@ -184,8 +184,6 @@
},
],
width: 150,
titleSlotName: 'typeTitle',
typeTitleTooltip: t('apiScenario.params.typeTooltip'),
},
{
title: 'apiScenario.params.paramValue',

View File

@ -1,205 +1,231 @@
<!-- eslint-disable prefer-destructuring -->
<template>
<!-- 用例表开始 -->
<MsAdvanceFilter
v-model:keyword="keyword"
:filter-config-list="filterConfigList"
:custom-fields-config-list="searchCustomFields"
:search-placeholder="t('caseManagement.featureCase.searchPlaceholder')"
:row-count="filterRowCount"
@keyword-search="fetchData"
@adv-search="handleAdvSearch"
@refresh="fetchData()"
>
<template #left>
<a-popover title="" position="bottom">
<div class="show-table-top-title">
<div class="one-line-text max-h-[32px] max-w-[300px] text-[var(--color-text-1)]">
{{ moduleNamePath }}
</div>
<span class="text-[var(--color-text-4)]"> ({{ props.modulesCount[props.activeFolder] || 0 }})</span>
</div>
<template #content>
<div class="max-w-[400px] text-[14px] font-medium text-[var(--color-text-1)]">
{{ moduleNamePath }}
<span class="text-[var(--color-text-4)]">({{ props.modulesCount[props.activeFolder] || 0 }})</span>
</div>
</template>
</a-popover>
</template>
<template #right>
<!-- TODO 暂时先不展示了 -->
<!-- <a-radio-group v-model:model-value="showType" type="button" class="file-show-type">
<a-radio value="list" class="show-type-icon p-[2px]"><MsIcon type="icon-icon_view-list_outlined" /></a-radio>
<a-radio value="xMind" class="show-type-icon p-[2px]"><MsIcon type="icon-icon_mindnote_outlined" /></a-radio>
</a-radio-group> -->
</template>
</MsAdvanceFilter>
<ms-base-table
v-if="showType === 'list'"
v-bind="propsRes"
ref="tableRef"
filter-icon-align-left
class="mt-[16px]"
:action-config="tableBatchActions"
@selected-change="handleTableSelect"
v-on="propsEvent"
@batch-action="handleTableBatch"
@change="changeHandler"
@module-change="initData()"
@cell-click="handleCellClick"
>
<template #num="{ record }">
<span type="text" class="one-line-text cursor-pointer px-0 text-[rgb(var(--primary-5))]">{{ record.num }}</span>
</template>
<template #name="{ record }">
<div class="one-line-text">{{ characterLimit(record.name) }}</div>
</template>
<template #caseLevel="{ record }">
<a-select
v-model:model-value="record.caseLevel"
:placeholder="t('common.pleaseSelect')"
class="param-input w-full"
@click.stop
@change="() => handleStatusChange(record)"
<div class="h-full">
<!-- 用例表开始 -->
<template v-if="showType === 'list'">
<MsAdvanceFilter
v-model:keyword="keyword"
:filter-config-list="filterConfigList"
:custom-fields-config-list="searchCustomFields"
:search-placeholder="t('caseManagement.featureCase.searchPlaceholder')"
:row-count="filterRowCount"
@keyword-search="fetchData"
@adv-search="handleAdvSearch"
@refresh="fetchData()"
>
<template #label>
<span class="text-[var(--color-text-2)]"> <caseLevel :case-level="record.caseLevel" /></span>
<template #left>
<a-popover title="" position="bottom">
<div class="show-table-top-title">
<div class="one-line-text max-h-[32px] max-w-[300px] text-[var(--color-text-1)]">
{{ moduleNamePath }}
</div>
<span class="text-[var(--color-text-4)]"> ({{ props.modulesCount[props.activeFolder] || 0 }})</span>
</div>
<template #content>
<div class="max-w-[400px] text-[14px] font-medium text-[var(--color-text-1)]">
{{ moduleNamePath }}
<span class="text-[var(--color-text-4)]">({{ props.modulesCount[props.activeFolder] || 0 }})</span>
</div>
</template>
</a-popover>
</template>
<a-option v-for="item of caseLevelList" :key="item.value" :value="item.value">
<caseLevel :case-level="item.text" />
</a-option>
</a-select>
</template>
<!-- 用例等级 -->
<template #[FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL]="{ filterContent }">
<caseLevel :case-level="filterContent.text" />
</template>
<!-- 执行结果 -->
<template #[FilterSlotNameEnum.CASE_MANAGEMENT_EXECUTE_RESULT]="{ filterContent }">
<ExecuteStatusTag :execute-result="filterContent.value" />
</template>
<!-- 评审结果 -->
<template #[FilterSlotNameEnum.CASE_MANAGEMENT_REVIEW_RESULT]="{ filterContent }">
<MsIcon
:type="statusIconMap[filterContent.value]?.icon"
class="mr-1"
:class="[statusIconMap[filterContent.value].color]"
></MsIcon>
<span>{{ statusIconMap[filterContent.value]?.statusText }} </span>
</template>
<template #reviewStatus="{ record }">
<MsIcon
:type="statusIconMap[record.reviewStatus]?.icon || ''"
class="mr-1"
:class="[statusIconMap[record.reviewStatus].color]"
></MsIcon>
<span>{{ statusIconMap[record.reviewStatus]?.statusText || '' }} </span>
</template>
<template #lastExecuteResult="{ record }">
<ExecuteStatusTag :execute-result="record.lastExecuteResult" />
</template>
<template #moduleId="{ record }">
<a-tree-select
v-if="record.showModuleTree"
v-model:modelValue="record.moduleId"
dropdown-class-name="tree-dropdown"
class="param-input w-full"
:data="caseTreeData"
:allow-search="true"
:field-names="{
title: 'name',
key: 'id',
children: 'children',
}"
:tree-props="{
virtualListProps: {
height: 200,
},
}"
@click.stop
@change="(value) => handleChangeModule(record, value)"
<template #right>
<a-radio-group v-model:model-value="showType" type="button" size="small" class="list-show-type">
<a-radio value="list" class="show-type-icon !m-[2px]">
<MsIcon :size="14" type="icon-icon_view-list_outlined" />
</a-radio>
<a-radio value="xMind" class="show-type-icon !m-[2px]">
<MsIcon :size="14" type="icon-icon_mindnote_outlined" />
</a-radio>
</a-radio-group>
</template>
</MsAdvanceFilter>
<ms-base-table
v-bind="propsRes"
ref="tableRef"
filter-icon-align-left
class="mt-[16px]"
:action-config="tableBatchActions"
@selected-change="handleTableSelect"
v-on="propsEvent"
@batch-action="handleTableBatch"
@change="changeHandler"
@module-change="initData()"
@cell-click="handleCellClick"
>
<template #tree-slot-title="node">
<a-tooltip :content="`${node.name}`" position="tl">
<div class="one-line-text max-w-[200px] text-[var(--color-text-1)]">{{ node.name }}</div>
<template #num="{ record }">
<span type="text" class="one-line-text cursor-pointer px-0 text-[rgb(var(--primary-5))]">{{
record.num
}}</span>
</template>
<template #name="{ record }">
<div class="one-line-text">{{ characterLimit(record.name) }}</div>
</template>
<template #caseLevel="{ record }">
<a-select
v-model:model-value="record.caseLevel"
:placeholder="t('common.pleaseSelect')"
class="param-input w-full"
@click.stop
@change="() => handleStatusChange(record)"
>
<template #label>
<span class="text-[var(--color-text-2)]"> <caseLevel :case-level="record.caseLevel" /></span>
</template>
<a-option v-for="item of caseLevelList" :key="item.value" :value="item.value">
<caseLevel :case-level="item.text" />
</a-option>
</a-select>
</template>
<!-- 用例等级 -->
<template #[FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL]="{ filterContent }">
<caseLevel :case-level="filterContent.text" />
</template>
<!-- 执行结果 -->
<template #[FilterSlotNameEnum.CASE_MANAGEMENT_EXECUTE_RESULT]="{ filterContent }">
<ExecuteStatusTag :execute-result="filterContent.value" />
</template>
<!-- 评审结果 -->
<template #reviewStatus="{ record }">
<MsIcon
:type="statusIconMap[record.reviewStatus]?.icon || ''"
class="mr-1"
:class="[statusIconMap[record.reviewStatus].color]"
></MsIcon>
<span>{{ statusIconMap[record.reviewStatus]?.statusText || '' }} </span>
</template>
<template #lastExecuteResult="{ record }">
<executeResult :execute-result="record.lastExecuteResult" />
</template>
<template #moduleId="{ record }">
<a-tree-select
v-if="record.showModuleTree"
v-model:modelValue="record.moduleId"
dropdown-class-name="tree-dropdown"
class="param-input w-full"
:data="caseTreeData"
:allow-search="true"
:field-names="{
title: 'name',
key: 'id',
children: 'children',
}"
:tree-props="{
virtualListProps: {
height: 200,
},
}"
@click.stop
@change="(value) => handleChangeModule(record, value)"
>
<template #tree-slot-title="node">
<a-tooltip :content="`${node.name}`" position="tl">
<div class="one-line-text max-w-[200px] text-[var(--color-text-1)]">{{ node.name }}</div>
</a-tooltip>
</template>
</a-tree-select>
<a-tooltip v-else :content="getModules(record.moduleId)" position="top">
<span class="one-line-text inline-block" @click.stop="record.showModuleTree = true">
{{ getModules(record.moduleId) }}
</span>
</a-tooltip>
</template>
</a-tree-select>
<a-tooltip v-else :content="getModules(record.moduleId)" position="top">
<span class="one-line-text inline-block" @click.stop="record.showModuleTree = true">
{{ getModules(record.moduleId) }}
</span>
</a-tooltip>
</template>
<!-- 渲染自定义字段开始TODO -->
<template v-for="item in customFieldsColumns" :key="item.slotName" #[item.slotName]="{ record }">
<a-tooltip
:content="getTableFields(record.customFields, item as MsTableColumn, record.createUser)"
position="top"
:mouse-enter-delay="100"
mini
>
<div class="one-line-text max-w-[300px]">{{
getTableFields(record.customFields, item as MsTableColumn, record.createUser)
}}</div>
</a-tooltip>
</template>
<!-- 渲染自定义字段结束 -->
<template #operation="{ record }">
<MsButton v-permission="['FUNCTIONAL_CASE:READ+UPDATE']" class="!mr-0" @click="operateCase(record, 'edit')">
{{ t('common.edit') }}
</MsButton>
<a-divider
v-permission="['FUNCTIONAL_CASE:READ+UPDATE']"
class="!mx-2 h-[12px]"
direction="vertical"
:margin="8"
></a-divider>
<MsButton v-permission="['FUNCTIONAL_CASE:READ+ADD']" class="!mr-0" @click="operateCase(record, 'copy')">
{{ t('caseManagement.featureCase.copy') }}
</MsButton>
<a-divider
v-permission="['FUNCTIONAL_CASE:READ+ADD']"
class="!mx-2 h-[12px]"
direction="vertical"
:margin="8"
></a-divider>
<span v-permission="['FUNCTIONAL_CASE:READ+DELETE']">
<MsTableMoreAction :list="moreActions" @select="handleMoreActionSelect($event, record)" />
</span>
</template>
<!-- 渲染自定义字段开始TODO -->
<template v-for="item in customFieldsColumns" :key="item.slotName" #[item.slotName]="{ record }">
<a-tooltip
:content="getTableFields(record.customFields, item as MsTableColumn, record.createUser)"
position="top"
:mouse-enter-delay="100"
mini
>
<div class="one-line-text max-w-[300px]">{{
getTableFields(record.customFields, item as MsTableColumn, record.createUser)
}}</div>
</a-tooltip>
</template>
<!-- 渲染自定义字段结束 -->
<template #operation="{ record }">
<MsButton v-permission="['FUNCTIONAL_CASE:READ+UPDATE']" class="!mr-0" @click="operateCase(record, 'edit')">
{{ t('common.edit') }}
</MsButton>
<a-divider
v-permission="['FUNCTIONAL_CASE:READ+UPDATE']"
class="!mx-2 h-[12px]"
direction="vertical"
:margin="8"
></a-divider>
<MsButton v-permission="['FUNCTIONAL_CASE:READ+ADD']" class="!mr-0" @click="operateCase(record, 'copy')">
{{ t('caseManagement.featureCase.copy') }}
</MsButton>
<a-divider
v-permission="['FUNCTIONAL_CASE:READ+ADD']"
class="!mx-2 h-[12px]"
direction="vertical"
:margin="8"
></a-divider>
<span v-permission="['FUNCTIONAL_CASE:READ+DELETE']">
<MsTableMoreAction :list="moreActions" @select="handleMoreActionSelect($event, record)" />
</span>
</template>
<template v-if="(keyword || '').trim() === ''" #empty>
<div class="flex w-full items-center justify-center p-[8px] text-[var(--color-text-4)]">
{{ t('caseManagement.caseReview.tableNoData') }}
<MsButton v-permission="['FUNCTIONAL_CASE:READ+ADD']" class="ml-[8px]" @click="createCase">
{{ t('caseManagement.featureCase.creatingCase') }}
</MsButton>
{{ t('caseManagement.featureCase.or') }}
<MsButton v-permission="['FUNCTIONAL_CASE:READ+IMPORT']" class="ml-[8px]" @click="emit('import', 'Excel')">
{{ t('caseManagement.featureCase.importExcel') }}
</MsButton>
<!-- <MsButton class="ml-[4px]" @click="emit('import', 'Xmind')">
{{ t('caseManagement.featureCase.importXmind') }}
</MsButton> -->
</div>
<template v-if="(keyword || '').trim() === ''" #empty>
<div class="flex w-full items-center justify-center p-[8px] text-[var(--color-text-4)]">
{{ t('caseManagement.caseReview.tableNoData') }}
<MsButton v-permission="['FUNCTIONAL_CASE:READ+ADD']" class="ml-[8px]" @click="createCase">
{{ t('caseManagement.featureCase.creatingCase') }}
</MsButton>
{{ t('caseManagement.featureCase.or') }}
<MsButton v-permission="['FUNCTIONAL_CASE:READ+IMPORT']" class="ml-[8px]" @click="emit('import', 'Excel')">
{{ t('caseManagement.featureCase.importExcel') }}
</MsButton>
<!-- <MsButton class="ml-[4px]" @click="emit('import', 'Xmind')">
{{ t('caseManagement.featureCase.importXmind') }}
</MsButton> -->
</div>
</template>
</ms-base-table>
</template>
</ms-base-table>
<!-- 用例表结束 -->
<!-- 脑图开始 -->
<MinderEditor
v-else
:import-json="importJson"
:tags="['模块', '用例', '前置条件', '备注', '步骤', '预期结果']"
tag-enable
sequence-enable
@node-click="handleNodeClick"
/>
<MsDrawer v-model:visible="visible" :width="480" :mask="false">
{{ nodeData.text }}
</MsDrawer>
<!-- 脑图结束 -->
<!-- 用例表结束 -->
<div v-else class="h-full">
<div class="flex flex-row items-center justify-between">
<a-popover title="" position="bottom">
<div class="show-table-top-title">
<div class="one-line-text max-h-[32px] max-w-[300px] text-[var(--color-text-1)]">
{{ moduleNamePath }}
</div>
<span class="text-[var(--color-text-4)]"> ({{ props.modulesCount[props.activeFolder] || 0 }})</span>
</div>
<template #content>
<div class="max-w-[400px] text-[14px] font-medium text-[var(--color-text-1)]">
{{ moduleNamePath }}
<span class="text-[var(--color-text-4)]">({{ props.modulesCount[props.activeFolder] || 0 }})</span>
</div>
</template>
</a-popover>
<div class="flex items-center gap-[12px]">
<a-radio-group v-model:model-value="showType" type="button" size="small" class="list-show-type">
<a-radio value="list" class="show-type-icon !m-[2px]">
<MsIcon :size="14" type="icon-icon_view-list_outlined" />
</a-radio>
<a-radio value="xMind" class="show-type-icon !m-[2px]">
<MsIcon :size="14" type="icon-icon_mindnote_outlined" />
</a-radio>
</a-radio-group>
<MsTag no-margin size="large" class="cursor-pointer" theme="outline" @click="fetchData">
<MsIcon class="text-[16px] text-[var(color-text-4)]" :size="32" type="icon-icon_reset_outlined" />
</MsTag>
</div>
</div>
<div class="h-[calc(100%-32px)]">
<!-- 脑图开始 -->
<MsMinder minder-type="FeatureCase" :import-json="importJson" @node-click="handleNodeClick" />
<MsDrawer v-model:visible="visible" :width="480" :mask="false">
{{ nodeData.text }}
</MsDrawer>
<!-- 脑图结束 -->
</div>
</div>
</div>
<a-modal
v-model:visible="showBatchMoveDrawer"
title-align="start"
@ -292,14 +318,13 @@
import MsButton from '@/components/pure/ms-button/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
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 } 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 caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import ExecuteStatusTag from '@/components/business/ms-case-associate/executeResult.vue';
import executeResult from '@/components/business/ms-case-associate/executeResult.vue';
import BatchEditModal from './batchEditModal.vue';
import CaseDetailDrawer from './caseDetailDrawer.vue';
import FeatureCaseTree from './caseTree.vue';
@ -337,7 +362,6 @@
DemandItem,
DragCase,
} from '@/models/caseManagement/featureCase';
import type { TableQueryParams } from '@/models/common';
import { ModuleTreeNode } from '@/models/common';
import { CaseManagementRouteEnum } from '@/enums/routeEnum';
import { ColumnEditTypeEnum, TableKeyEnum } from '@/enums/tableEnum';
@ -1566,4 +1590,10 @@
width: 200px !important;
}
}
.list-show-type {
padding: 0;
:deep(.arco-radio-button-content) {
padding: 4px 6px;
}
}
</style>

View File

@ -105,7 +105,7 @@
</div>
</template>
<template #second>
<div class="p-[16px_16px]">
<div class="h-full p-[16px_16px]">
<CaseTable
ref="caseTableRef"
:active-folder="activeFolder"