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 { .arco-radio-button {
@apply bg-transparent; @apply bg-transparent;
margin: 1px; line-height: 20px;
} }
.arco-radio-checked { .arco-radio-checked {
@apply bg-white; @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> <template>
<div class="flex flex-row items-center justify-between"> <div class="flex flex-row items-center justify-between">
<slot name="left"></slot> <slot name="left"></slot>
<div class="flex flex-row gap-[8px]"> <div class="flex flex-row gap-[12px]">
<a-input-search <a-input-search
v-model:modelValue="innerKeyword" v-model:modelValue="innerKeyword"
size="small" size="small"

View File

@ -1,7 +1,22 @@
<template> <template>
<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"> <div class="mind-tab-panel">
<edit-menu <editMenu
:minder="minder" :minder="minder"
:move-enable="props.moveEnable" :move-enable="props.moveEnable"
:move-confirm="props.moveConfirm" :move-confirm="props.moveConfirm"
@ -17,18 +32,24 @@
:tags="props.tags" :tags="props.tags"
:distinct-tags="props.distinctTags" :distinct-tags="props.distinctTags"
:del-confirm="props.delConfirm" :del-confirm="props.delConfirm"
:replaceable-tags="props.replaceableTags"
:single-tag="props.singleTag"
:insert-node="props.insertNode"
:after-tag-edit="props.afterTagEdit"
/> />
</div> </div>
</header>
</template> </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 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({ const props = defineProps({
...editMenuProps, ...editMenuProps,
...insertProps,
...moleProps, ...moleProps,
...priorityProps, ...priorityProps,
...tagProps, ...tagProps,
@ -36,6 +57,21 @@
...viewMenuProps, ...viewMenuProps,
minder: null, 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> </script>
<style lang="less"> <style lang="less">
@ -46,4 +82,21 @@
background-repeat: no-repeat; 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> </style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,9 +2,35 @@
* Api * 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 = { export const mainEditorProps = {
importJson: { importJson: {
type: Object, type: Object as PropType<MinderJson>,
default() { default() {
return { return {
root: {}, root: {},
@ -20,6 +46,12 @@ export const mainEditorProps = {
disabled: Boolean, disabled: Boolean,
}; };
export const headerProps = {
iconButtons: {
type: [] as PropType<MinderIconButtonItem[]>,
},
};
export const priorityProps = { export const priorityProps = {
priorityCount: { priorityCount: {
type: Number, type: Number,
@ -39,27 +71,45 @@ export const priorityProps = {
type: String, type: String,
default: 'P', default: 'P',
}, },
priorityDisableCheck: Function, priorityDisableCheck: Function as PropType<(node: MinderJsonNode) => boolean>,
operators: [], operators: [],
}; };
export interface MinderReplaceTag {
tags: string[];
condition: (node: MinderJsonNode, tags: string[]) => boolean;
}
export const tagProps = { export const tagProps = {
tags: { tags: {
// 自定义标签 // 自定义标签
type: Array<string>, type: Array<string>,
default() { default() {
return [] as string[]; return [];
}, },
}, },
distinctTags: { distinctTags: {
// 个别标签二选一 // 个别标签二选一
type: Array<string>, type: Array<string>,
default() { default() {
return [] as string[]; return [];
}, },
}, },
singleTag: {
// 单标签
type: Boolean,
default: false,
},
replaceableTags: Function as PropType<(node: MinderJsonNode) => string[]>,
tagDisableCheck: Function, 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 = { export const editMenuProps = {

View File

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

View File

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

View File

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

View File

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

View File

@ -252,7 +252,7 @@
JSONPath({ JSONPath({
json: parseJson.value, json: parseJson.value,
path: expressionForm.value.expression, 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) { } catch (error) {
matchResult.value = JSONPath({ json: props.response || '', path: expressionForm.value.expression }) || []; matchResult.value = JSONPath({ json: props.response || '', path: expressionForm.value.expression }) || [];
} }

View File

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

View File

@ -1,6 +1,8 @@
<!-- eslint-disable prefer-destructuring --> <!-- eslint-disable prefer-destructuring -->
<template> <template>
<div class="h-full">
<!-- 用例表开始 --> <!-- 用例表开始 -->
<template v-if="showType === 'list'">
<MsAdvanceFilter <MsAdvanceFilter
v-model:keyword="keyword" v-model:keyword="keyword"
:filter-config-list="filterConfigList" :filter-config-list="filterConfigList"
@ -28,15 +30,17 @@
</a-popover> </a-popover>
</template> </template>
<template #right> <template #right>
<!-- TODO 暂时先不展示了 --> <a-radio-group v-model:model-value="showType" type="button" size="small" class="list-show-type">
<!-- <a-radio-group v-model:model-value="showType" type="button" class="file-show-type"> <a-radio value="list" class="show-type-icon !m-[2px]">
<a-radio value="list" class="show-type-icon p-[2px]"><MsIcon type="icon-icon_view-list_outlined" /></a-radio> <MsIcon :size="14" type="icon-icon_view-list_outlined" />
<a-radio value="xMind" class="show-type-icon p-[2px]"><MsIcon type="icon-icon_mindnote_outlined" /></a-radio> </a-radio>
</a-radio-group> --> <a-radio value="xMind" class="show-type-icon !m-[2px]">
<MsIcon :size="14" type="icon-icon_mindnote_outlined" />
</a-radio>
</a-radio-group>
</template> </template>
</MsAdvanceFilter> </MsAdvanceFilter>
<ms-base-table <ms-base-table
v-if="showType === 'list'"
v-bind="propsRes" v-bind="propsRes"
ref="tableRef" ref="tableRef"
filter-icon-align-left filter-icon-align-left
@ -50,7 +54,9 @@
@cell-click="handleCellClick" @cell-click="handleCellClick"
> >
<template #num="{ record }"> <template #num="{ record }">
<span type="text" class="one-line-text cursor-pointer px-0 text-[rgb(var(--primary-5))]">{{ record.num }}</span> <span type="text" class="one-line-text cursor-pointer px-0 text-[rgb(var(--primary-5))]">{{
record.num
}}</span>
</template> </template>
<template #name="{ record }"> <template #name="{ record }">
<div class="one-line-text">{{ characterLimit(record.name) }}</div> <div class="one-line-text">{{ characterLimit(record.name) }}</div>
@ -80,14 +86,6 @@
<ExecuteStatusTag :execute-result="filterContent.value" /> <ExecuteStatusTag :execute-result="filterContent.value" />
</template> </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 }"> <template #reviewStatus="{ record }">
<MsIcon <MsIcon
:type="statusIconMap[record.reviewStatus]?.icon || ''" :type="statusIconMap[record.reviewStatus]?.icon || ''"
@ -97,7 +95,7 @@
<span>{{ statusIconMap[record.reviewStatus]?.statusText || '' }} </span> <span>{{ statusIconMap[record.reviewStatus]?.statusText || '' }} </span>
</template> </template>
<template #lastExecuteResult="{ record }"> <template #lastExecuteResult="{ record }">
<ExecuteStatusTag :execute-result="record.lastExecuteResult" /> <executeResult :execute-result="record.lastExecuteResult" />
</template> </template>
<template #moduleId="{ record }"> <template #moduleId="{ record }">
<a-tree-select <a-tree-select
@ -186,20 +184,48 @@
</div> </div>
</template> </template>
</ms-base-table> </ms-base-table>
</template>
<!-- 用例表结束 --> <!-- 用例表结束 -->
<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)]">
<!-- 脑图开始 --> <!-- 脑图开始 -->
<MinderEditor <MsMinder minder-type="FeatureCase" :import-json="importJson" @node-click="handleNodeClick" />
v-else
:import-json="importJson"
:tags="['模块', '用例', '前置条件', '备注', '步骤', '预期结果']"
tag-enable
sequence-enable
@node-click="handleNodeClick"
/>
<MsDrawer v-model:visible="visible" :width="480" :mask="false"> <MsDrawer v-model:visible="visible" :width="480" :mask="false">
{{ nodeData.text }} {{ nodeData.text }}
</MsDrawer> </MsDrawer>
<!-- 脑图结束 --> <!-- 脑图结束 -->
</div>
</div>
</div>
<a-modal <a-modal
v-model:visible="showBatchMoveDrawer" v-model:visible="showBatchMoveDrawer"
title-align="start" title-align="start"
@ -292,14 +318,13 @@
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue'; import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/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 MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } 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 useTable from '@/components/pure/ms-table/useTable';
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue'; import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types'; import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue'; 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 BatchEditModal from './batchEditModal.vue';
import CaseDetailDrawer from './caseDetailDrawer.vue'; import CaseDetailDrawer from './caseDetailDrawer.vue';
import FeatureCaseTree from './caseTree.vue'; import FeatureCaseTree from './caseTree.vue';
@ -337,7 +362,6 @@
DemandItem, DemandItem,
DragCase, DragCase,
} from '@/models/caseManagement/featureCase'; } from '@/models/caseManagement/featureCase';
import type { TableQueryParams } from '@/models/common';
import { ModuleTreeNode } from '@/models/common'; import { ModuleTreeNode } from '@/models/common';
import { CaseManagementRouteEnum } from '@/enums/routeEnum'; import { CaseManagementRouteEnum } from '@/enums/routeEnum';
import { ColumnEditTypeEnum, TableKeyEnum } from '@/enums/tableEnum'; import { ColumnEditTypeEnum, TableKeyEnum } from '@/enums/tableEnum';
@ -1566,4 +1590,10 @@
width: 200px !important; width: 200px !important;
} }
} }
.list-show-type {
padding: 0;
:deep(.arco-radio-button-content) {
padding: 4px 6px;
}
}
</style> </style>

View File

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