feat(脑图): 脑图节点菜单&空白节点&用例评审未评审数量

This commit is contained in:
baiqi 2024-06-07 18:42:11 +08:00 committed by Craftsman
parent 3fe1bc68dd
commit 8ae4c91500
30 changed files with 1205 additions and 459 deletions

View File

@ -42,6 +42,7 @@ import {
GetAssociationPublicCasePageUrl,
GetAssociationPublicModuleTreeUrl,
GetCaseListUrl,
GetCaseMinderTreeUrl,
GetCaseMinderUrl,
GetCaseModulesCountUrl,
GetCaseModuleTreeUrl,
@ -191,6 +192,11 @@ export function getCaseMinder(data: { projectId: string; moduleId: string }) {
return MSR.post<MinderJsonNode[]>({ url: `${GetCaseMinderUrl}`, data });
}
// 获取脑图模块树(包含文本节点)
export function getCaseMinderTree(data: { projectId: string; moduleId: string }) {
return MSR.post<MinderJsonNode[]>({ url: `${GetCaseMinderTreeUrl}`, data });
}
// 回收站
// 回收站用例分页表

View File

@ -28,6 +28,7 @@ export const GetSearchCustomFieldsUrl = '/functional/case/custom/field';
export const GetAssociatedFilePageUrl = '/attachment/page';
export const SaveCaseMinderUrl = '/functional/mind/case/edit'; // 保存用例脑图
export const GetCaseMinderUrl = '/functional/mind/case/list'; // 获取脑图数据
export const GetCaseMinderTreeUrl = '/functional/mind/case/tree'; // 获取脑图模块树(含文本节点)
// 获取模块树
export const GetCaseModuleTreeUrl = '/functional/case/module/tree';

View File

@ -1,7 +1,7 @@
@font-face {
font-family: iconfont; /* Project id 3462279 */
src: url('iconfont.woff2?t=1717669877554') format('woff2'), url('iconfont.woff?t=1717669877554') format('woff'),
url('iconfont.ttf?t=1717669877554') format('truetype'), url('iconfont.svg?t=1717669877554#iconfont') format('svg');
src: url('iconfont.woff2?t=1717664244652') format('woff2'), url('iconfont.woff?t=1717664244652') format('woff'),
url('iconfont.ttf?t=1717664244652') format('truetype'), url('iconfont.svg?t=1717664244652#iconfont') format('svg');
}
.iconfont {
font-size: 16px;

View File

@ -469,6 +469,10 @@
/** radio **/
.arco-radio-group-button {
.arco-radio-button-content {
@apply break-keep;
}
background-color: var(--color-text-n8);
.arco-radio-button {
@apply bg-transparent;

View File

@ -3,13 +3,20 @@
v-model:activeExtraKey="activeExtraKey"
v-model:extra-visible="extraVisible"
v-model:loading="loading"
v-model:import-json="importJson"
:tags="[]"
:import-json="importJson"
:replaceable-tags="replaceableTags"
:insert-node="insertNode"
:priority-disable-check="priorityDisableCheck"
:after-tag-edit="afterTagEdit"
:extract-content-tab-list="extractContentTabList"
:can-show-enter-node="canShowEnterNode"
:insert-sibling-menus="insertSiblingMenus"
:insert-son-menus="insertSonMenus"
:can-show-paste-menu="!stopPaste()"
:can-show-more-menu="canShowMoreMenu()"
:can-show-priority-menu="canShowPriorityMenu()"
:priority-tooltip="t('caseManagement.caseReview.caseLevel')"
single-tag
tag-enable
sequence-enable
@ -19,6 +26,18 @@
@before-exec-command="handleBeforeExecCommand"
@save="handleMinderSave"
>
<template #extractMenu>
<a-tooltip v-if="showDetailMenu" :content="t('common.detail')">
<MsButton
type="icon"
class="ms-minder-node-float-menu-icon-button"
:class="[extraVisible ? 'ms-minder-node-float-menu-icon-button--focus' : '']"
@click="toggleDetail"
>
<MsIcon type="icon-icon_describe_outlined" class="text-[var(--color-text-4)]" />
</MsButton>
</a-tooltip>
</template>
<template #extractTabContent>
<baseInfo
v-if="activeExtraKey === 'baseInfo'"
@ -43,9 +62,11 @@
<script setup lang="ts">
import { Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import { FormItem } from '@/components/pure/ms-form-create/types';
import MsMinderEditor from '@/components/pure/ms-minder-editor/minderEditor.vue';
import type {
InsertMenuItem,
MinderEvent,
MinderJson,
MinderJsonNode,
@ -61,11 +82,12 @@
checkFileIsUpdateRequest,
getCaseDetail,
getCaseMinder,
getCaseModuleTree,
getCaseMinderTree,
saveCaseMinder,
} from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import useMinderStore from '@/store/modules/components/minder-editor/index';
import { MinderCustomEvent } from '@/store/modules/components/minder-editor/types';
import { filterTree, getGenerateId, mapTree } from '@/utils';
@ -87,18 +109,19 @@
const appStore = useAppStore();
const { t } = useI18n();
const minderStore = useMinderStore();
const caseTag = t('common.case');
const moduleTag = t('common.module');
const topTags = [moduleTag, caseTag];
const stepTag = t('ms.minders.stepDesc');
const textTag = t('ms.minders.textDesc');
const textDescTag = t('ms.minders.textDesc');
const prerequisiteTag = t('ms.minders.precondition');
const stepExpectTag = t('ms.minders.stepExpect');
const remarkTag = t('ms.minders.remark');
const descTags = [stepTag, textTag];
const caseChildTags = [prerequisiteTag, stepTag, textTag, remarkTag];
const caseOffspringTags = [...caseChildTags, stepTag, stepExpectTag, textTag, remarkTag];
const descTags = [stepTag, textDescTag];
const caseChildTags = [prerequisiteTag, stepTag, textDescTag, remarkTag];
const caseOffspringTags = [...caseChildTags, stepTag, stepExpectTag, textDescTag, remarkTag];
const importJson = ref<MinderJson>({
root: {} as MinderJsonNode,
template: 'default',
@ -122,16 +145,16 @@
async function initCaseTree() {
try {
loading.value = true;
const res = await getCaseModuleTree({
const res = await getCaseMinderTree({
projectId: appStore.currentProjectId,
moduleId: props.moduleId === 'NONE' ? '' : props.moduleId,
moduleId: props.moduleId === 'all' ? '' : props.moduleId,
});
caseTree.value = mapTree<MinderJsonNode>(res, (e) => ({
...e,
data: {
id: e.id,
text: e.name,
resource: e.data?.id === 'fakeNode' ? [] : [moduleTag],
resource: props.modulesCount[e.id] !== undefined ? [moduleTag] : e.data?.resource,
expandState: e.level === 1 ? 'expand' : 'collapse',
count: props.modulesCount[e.id],
isNew: false,
@ -160,39 +183,13 @@
},
};
window.minder.importJson(importJson.value);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
/**
* 初始化模块下脑图数据
*/
async function initMinder() {
try {
loading.value = true;
const res = await getCaseMinder({
projectId: appStore.currentProjectId,
moduleId: props.moduleId === 'all' ? '' : props.moduleId,
});
importJson.value.root.children = mapTree(res, (node) => {
return {
...node,
data: {
...node.data,
isNew: false,
},
};
});
importJson.value.root.data = {
id: props.moduleId === 'all' ? '' : props.moduleId,
text: props.moduleName,
resource: [moduleTag],
};
window.minder.importJson(importJson.value);
if (props.moduleId !== 'all') {
nextTick(() => {
minderStore.dispatchEvent(MinderEventName.ENTER_NODE, undefined, undefined, undefined, [
window.minder.getNodeById(props.moduleId),
]);
});
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
@ -202,11 +199,7 @@
}
watchEffect(() => {
if (props.moduleId === 'all') {
initCaseTree();
} else {
initMinder();
}
initCaseTree();
});
const baseInfoRef = ref<InstanceType<typeof baseInfo>>();
@ -221,7 +214,7 @@
let remarkNode: MinderJsonNode | undefined; //
const stepNodes: MinderJsonNode[] = []; //
node.children?.forEach((item) => {
if (item.data?.resource?.includes(textTag)) {
if (item.data?.resource?.includes(textDescTag)) {
textStep = item;
} else if (item.data?.resource?.includes(stepTag)) {
stepNodes.push(item);
@ -268,8 +261,7 @@
/**
* 生成脑图保存的入参
*/
function makeMinderParams(): FeatureCaseMinderUpdateParams {
const fullJson: MinderJson = window.minder.exportJson();
function makeMinderParams(fullJson: MinderJson): FeatureCaseMinderUpdateParams {
filterTree(fullJson.root.children, (node, nodeIndex, parent) => {
if (node.data.isNew !== false || node.data.changed === true) {
if (node.data.resource?.includes(moduleTag)) {
@ -297,7 +289,7 @@
...caseNodeInfo,
});
return false; //
} else if (!node.data.resource) {
} else if (!node.data.resource || node.data.resource.length === 0) {
//
tempMinderParams.value.additionalNodeList.push({
id: node.data.id,
@ -308,16 +300,19 @@
});
}
}
return true;
});
return tempMinderParams.value;
}
async function handleMinderSave() {
async function handleMinderSave(fullJson: MinderJson, callback: () => void) {
try {
loading.value = true;
await saveCaseMinder(makeMinderParams());
await saveCaseMinder(makeMinderParams(fullJson));
Message.success(t('common.saveSuccess'));
initCaseTree();
callback();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
@ -510,14 +505,34 @@
execInert(type, child.data);
}
/**
* 插入指定的节点
* @param type 插入类型
* @param value 节点类型
*/
function insertSpecifyNode(type: string, value: string) {
execInert(type, {
id: getGenerateId(),
text: value !== t('ms.minders.text') ? value : '',
resource: value !== t('ms.minders.text') ? [value] : [],
expandState: 'expand',
isNew: true,
});
}
/**
* 插入节点
* @param node 目标节点
* @param type 插入类型
* @param value 插入值
*/
function insertNode(node: MinderJsonNode, type: string) {
function insertNode(node: MinderJsonNode, type: string, value?: string) {
switch (type) {
case 'AppendChildNode':
if (value) {
insertSpecifyNode('AppendChildNode', value);
break;
}
if (node.data?.resource?.includes(moduleTag)) {
execInert('AppendChildNode');
} else if (node.data?.resource?.includes(caseTag)) {
@ -534,7 +549,7 @@
const child = node.children[i];
if (child.data?.resource?.includes(prerequisiteTag)) {
hasPreCondition = true;
} else if (child.data?.resource?.includes(textTag)) {
} else if (child.data?.resource?.includes(textDescTag)) {
hasTextDesc = true;
} else if (child.data?.resource?.includes(remarkTag)) {
hasRemark = true;
@ -552,7 +567,7 @@
}
}
} else if (
(node.data?.resource?.includes(stepTag) || node.data?.resource?.includes(textTag)) &&
(node.data?.resource?.includes(stepTag) || node.data?.resource?.includes(textDescTag)) &&
(!node.children || node.children.length === 0)
) {
//
@ -566,6 +581,10 @@
}
break;
case 'AppendSiblingNode':
if (value) {
insertSpecifyNode('AppendSiblingNode', value);
break;
}
if (node.parent?.data?.resource?.includes(caseTag) && node.parent?.children) {
//
let hasPreCondition = false;
@ -577,7 +596,7 @@
hasPreCondition = true;
} else if (sibling.data?.resource?.includes(remarkTag)) {
hasRemark = true;
} else if (sibling.data?.resource?.includes(textTag)) {
} else if (sibling.data?.resource?.includes(textDescTag)) {
hasTextDesc = true;
}
}
@ -611,33 +630,6 @@
return true;
}
/**
* 标签编辑后如果将标签修改为模块则删除已添加的优先级
* @param node 选中节点
* @param tag 更改后的标签
*/
function afterTagEdit(node: MinderJsonNode, tag: string) {
if (tag === moduleTag && node.data) {
//
tempMinderParams.value.updateCaseList = tempMinderParams.value.updateCaseList.filter(
(e) => e.id !== node.data?.id
);
window.minder.execCommand('priority');
} else if (node.data?.resource?.includes(caseTag)) {
//
tempMinderParams.value.updateModuleList = tempMinderParams.value.updateModuleList.filter(
(e) => e.id !== node.data?.id
);
} else if (node.data?.resource?.some((e) => caseOffspringTags.includes(e))) {
//
if (node.parent?.data?.resource?.includes(caseTag)) {
node.parent.data.changed = true;
} else if (node.parent?.parent?.data?.resource?.includes(caseTag)) {
//
node.parent.parent.data.changed = true;
}
}
}
const baseInfoLoading = ref(false);
const formRules = ref<FormItem[]>([]);
@ -743,7 +735,7 @@
if (
resource?.includes(prerequisiteTag) ||
resource?.includes(stepTag) ||
resource?.includes(textTag) ||
resource?.includes(textDescTag) ||
resource?.includes(remarkTag)
) {
if (node.parent?.data) {
@ -756,29 +748,193 @@
}
}
const insertSiblingMenus = ref<InsertMenuItem[]>([]);
const insertSonMenus = ref<InsertMenuItem[]>([]);
/**
* 检测节点可展示的菜单项
* @param node 选中节点
*/
function checkNodeCanShowMenu(node: MinderJsonNode) {
const { data } = node;
if (data?.resource?.includes(moduleTag)) {
//
if (data?.id === 'NONE' || node.type === 'root' || node.parent?.data?.id === 'NONE') {
// NONENONE
insertSiblingMenus.value = [];
if (data?.id === 'NONE') {
// NONE
insertSonMenus.value = [
{
label: moduleTag,
value: moduleTag,
},
];
} else {
if (node.parent?.data?.id === 'NONE') {
// NONE
insertSiblingMenus.value = [
{
label: moduleTag,
value: moduleTag,
},
];
}
// NONE
insertSonMenus.value = [
{
label: moduleTag,
value: moduleTag,
},
{
label: caseTag,
value: caseTag,
},
{
label: t('ms.minders.text'),
value: t('ms.minders.text'),
},
];
}
} else {
//
insertSiblingMenus.value = [
{
label: moduleTag,
value: moduleTag,
},
{
label: caseTag,
value: caseTag,
},
{
label: t('ms.minders.text'),
value: t('ms.minders.text'),
},
];
//
insertSonMenus.value = [
{
label: moduleTag,
value: moduleTag,
},
{
label: caseTag,
value: caseTag,
},
{
label: t('ms.minders.text'),
value: t('ms.minders.text'),
},
];
}
} else if (data?.resource?.includes(caseTag)) {
//
insertSiblingMenus.value = [
{
label: moduleTag,
value: moduleTag,
},
{
label: caseTag,
value: caseTag,
},
{
label: t('ms.minders.text'),
value: t('ms.minders.text'),
},
];
insertSonMenus.value = caseChildTags.map((tag) => ({
label: tag,
value: tag,
}));
if (node.children?.some((child) => child.data?.resource?.includes(stepTag))) {
//
insertSonMenus.value = insertSonMenus.value.filter((e) => e.value !== textDescTag);
} else if (node.children?.some((child) => child.data?.resource?.includes(textDescTag))) {
//
insertSonMenus.value = insertSonMenus.value.filter((e) => e.value !== stepTag && e.value !== textDescTag);
}
if (node.children?.some((child) => child.data?.resource?.includes(prerequisiteTag))) {
//
insertSonMenus.value = insertSonMenus.value.filter((e) => e.value !== prerequisiteTag);
}
if (node.children?.some((child) => child.data?.resource?.includes(remarkTag))) {
//
insertSonMenus.value = insertSonMenus.value.filter((e) => e.value !== remarkTag);
}
} else if (data?.resource?.some((tag) => caseChildTags.includes(tag))) {
//
insertSiblingMenus.value = caseChildTags.map((tag) => ({
label: tag,
value: tag,
}));
if (node.parent?.children?.some((child) => child.data?.resource?.includes(stepTag))) {
//
insertSiblingMenus.value = insertSiblingMenus.value.filter((e) => e.value !== textDescTag);
} else if (node.parent?.children?.some((child) => child.data?.resource?.includes(textDescTag))) {
//
insertSiblingMenus.value = insertSiblingMenus.value.filter(
(e) => e.value !== stepTag && e.value !== textDescTag
);
}
if (node.parent?.children?.some((child) => child.data?.resource?.includes(prerequisiteTag))) {
//
insertSiblingMenus.value = insertSiblingMenus.value.filter((e) => e.value !== prerequisiteTag);
}
if (node.parent?.children?.some((child) => child.data?.resource?.includes(remarkTag))) {
//
insertSiblingMenus.value = insertSiblingMenus.value.filter((e) => e.value !== remarkTag);
}
if (
(data?.resource?.includes(textDescTag) || data?.resource?.includes(stepTag)) &&
(!node.children || node.children.length === 0)
) {
//
insertSonMenus.value = [
{
label: stepExpectTag,
value: stepExpectTag,
},
];
} else {
insertSonMenus.value = [];
}
} else {
insertSiblingMenus.value = [];
insertSonMenus.value = [];
}
}
const showDetailMenu = ref(false);
const canShowEnterNode = ref(false);
/**
* 处理脑图节点激活/点击
* @param node 被激活/点击的节点
*/
async function handleNodeSelect(node: MinderJsonNode) {
checkNodeCanShowMenu(node);
const { data } = node;
if (
data?.resource?.includes(moduleTag) &&
(node.children || []).length > 0 &&
node.type !== 'root' &&
!data.isNew
) {
//
canShowEnterNode.value = true;
} else {
canShowEnterNode.value = false;
}
if (data?.resource && data.resource.includes(caseTag)) {
extraVisible.value = true;
activeExtraKey.value = 'baseInfo';
resetExtractInfo();
if (data.isNew === false) {
//
initCaseDetail(data);
} else {
activeCase.value = {
id: data.id,
name: data.text,
isNew: true,
};
}
//
showDetailMenu.value = true;
} else if (data?.resource?.includes(moduleTag) && data.count > 0 && data.isLoaded !== true) {
//
try {
loading.value = true;
showDetailMenu.value = false;
extraVisible.value = false;
const res = await getCaseMinder({
projectId: appStore.currentProjectId,
moduleId: data.id,
@ -846,11 +1002,85 @@
loading.value = false;
}
} else {
//
extraVisible.value = false;
showDetailMenu.value = false;
resetExtractInfo();
if (node.children && node.children.length > 0) {
node.expand();
node.renderTree();
window.minder.layout();
}
}
}
/**
* 切换用例详情显示
*/
async function toggleDetail() {
extraVisible.value = !extraVisible.value;
const node: MinderJsonNode = window.minder.getSelectedNode();
const { data } = node;
if (extraVisible.value) {
if (data?.resource && data.resource.includes(caseTag)) {
activeExtraKey.value = 'baseInfo';
resetExtractInfo();
if (data.isNew === false) {
//
initCaseDetail(data);
} else {
activeCase.value = {
id: data.id,
name: data.text,
isNew: true,
};
}
}
}
}
/**
* 标签编辑后如果将标签修改为模块则删除已添加的优先级
* @param node 选中节点
* @param tag 更改后的标签
*/
function afterTagEdit(nodes: MinderJsonNode[], tag: string) {
nodes.forEach((node, index) => {
if (tag === moduleTag && node.data) {
//
tempMinderParams.value.updateCaseList = tempMinderParams.value.updateCaseList.filter(
(e) => e.id !== node.data?.id
);
node.data.isNew = true;
window.minder.execCommand('priority');
if (index === nodes.length - 1) {
nextTick(() => {
handleNodeSelect(node);
});
}
} else if (node.data?.resource?.includes(caseTag)) {
//
tempMinderParams.value.updateModuleList = tempMinderParams.value.updateModuleList.filter(
(e) => e.id !== node.data?.id
);
node.data.isNew = true;
if (index === nodes.length - 1) {
nextTick(() => {
handleNodeSelect(node);
});
}
} else if (node.data?.resource?.some((e) => caseOffspringTags.includes(e))) {
//
if (node.parent?.data?.resource?.includes(caseTag)) {
node.parent.data.changed = true;
} else if (node.parent?.parent?.data?.resource?.includes(caseTag)) {
//
node.parent.parent.data.changed = true;
}
}
});
}
/**
* 处理脑图节点操作
* @param event 脑图事件对象
@ -860,12 +1090,16 @@
if (nodes && nodes.length > 0) {
switch (name) {
case MinderEventName.DELETE_NODE:
case MinderEventName.CUT_NODE:
// TODO:
nodes.forEach((node) => {
tempMinderParams.value.deleteResourceList.push({
id: node.data?.id || getGenerateId(),
type: node.data?.resource?.[0] || moduleTag,
});
if (!caseOffspringTags.some((e) => node.data?.resource?.includes(e))) {
//
tempMinderParams.value.deleteResourceList.push({
id: node.data?.id || getGenerateId(),
type: node.data?.resource?.[0] || moduleTag,
});
}
if (node.data?.resource?.includes(caseTag)) {
//
tempMinderParams.value.updateCaseList = tempMinderParams.value.updateCaseList.filter(
@ -890,6 +1124,22 @@
}
}
function canShowMoreMenu() {
if (window.minder) {
const node: MinderJsonNode = window.minder.getSelectedNode();
return node?.data?.id !== 'NONE';
}
return false;
}
function canShowPriorityMenu() {
if (window.minder) {
const node: MinderJsonNode = window.minder.getSelectedNode();
return node?.data?.resource?.includes(caseTag);
}
return false;
}
/**
* 是否停止拖拽动作
* @param dragNode 拖动节点
@ -968,6 +1218,79 @@
return true;
}
/**
* 是否停止粘贴动作
*/
function stopPaste() {
const nodes = minderStore.clipboard;
if (window.minder) {
const node: MinderJsonNode = window.minder.getSelectedNode();
if (!node) {
return true;
}
if (node.data?.resource?.includes(moduleTag)) {
// NONE
if (node.data?.id === 'NONE' && nodes.every((e) => e.data?.resource?.includes(moduleTag))) {
return false;
}
//
if (
node.data?.id !== 'NONE' &&
nodes.some(
(e) => !e.data?.resource || e.data.resource.includes(moduleTag) || e.data.resource.includes(caseTag)
)
) {
return false;
}
}
if (node.data?.resource?.includes(caseTag)) {
if (nodes.every((e) => caseChildTags.some((item) => e.data?.resource?.includes(item)))) {
//
if (
nodes.length >= 1 &&
nodes.every((e) => e.data?.resource?.includes(stepTag)) &&
!node.children?.some((child) => child.data?.resource?.includes(textDescTag))
) {
// 1
return false;
}
if (nodes.length === 1) {
// 1
if (
node.children?.every((child) => !child.data?.resource?.includes(prerequisiteTag)) &&
nodes[0].data?.resource?.includes(prerequisiteTag)
) {
//
return false;
}
if (
node.children?.every((child) => !child.data?.resource?.includes(remarkTag)) &&
nodes[0].data?.resource?.includes(remarkTag)
) {
//
return false;
}
if (
node.children?.every((child) => !child.data?.resource?.includes(textDescTag)) &&
nodes[0].data?.resource?.includes(textDescTag)
) {
//
return false;
}
}
}
}
if ([stepTag, textDescTag].some((tag) => node.data?.resource?.includes(tag))) {
//
if (node.data?.resource?.includes(stepExpectTag)) {
//
return false;
}
}
}
return true;
}
/**
* 脑图命令执行前拦截
* @param event 命令执行事件
@ -995,6 +1318,20 @@
if (stopDrag(dragNodes, dropNode, 'arrange')) {
event.stopPropagation();
}
} else if (event.commandName === 'paste') {
if (stopPaste()) {
event.stopPropagation();
}
} else if (event.commandName === 'cut') {
minderStore.clipboard.forEach((node) => {
if (node.parent && node.parent.data?.resource?.includes(caseTag)) {
//
node.parent.data.changed = true;
} else if (node.parent?.parent && node.parent.parent.data?.resource?.includes(caseTag)) {
//
node.parent.parent.data.changed = true;
}
});
}
}
</script>

View File

@ -8,4 +8,5 @@ export default {
'ms.minders.caseName': 'Test Case Name',
'ms.minders.caseNameNotNull': 'Test Case Name cannot be empty',
'ms.minders.commentTotal': '{num} Comments in Total',
'ms.minders.text': 'Text',
};

View File

@ -8,4 +8,5 @@ export default {
'ms.minders.caseName': '用例名称',
'ms.minders.caseNameNotNull': '用例名称不能为空',
'ms.minders.commentTotal': '共 {num} 评论',
'ms.minders.text': '文本',
};

View File

@ -10,6 +10,7 @@ export interface UseEventListenerProps {
handleSelectionChange?: (node: MinderJsonNode) => void;
handleMinderEvent?: (event: MinderCustomEvent) => void;
handleBeforeExecCommand?: (event: MinderEvent) => void;
handleViewChange?: (event: MinderEvent) => void;
}
export default function useEventListener(listener: UseEventListenerProps) {
@ -35,15 +36,6 @@ export default function useEventListener(listener: UseEventListenerProps) {
}, 300)
);
// minder.on('dragStart', () => {
// const node: MinderJsonNode = minder.getSelectedNode();
// console.log('dragStart', node);
// });
// minder.on('dragFinish', () => {
// console.log('dragFinish', minder.history);
// });
// 监听脑图执行命令前可通过e.stopPropagation拦截命令执行
minder.on('beforeExecCommand', (e: MinderEvent) => {
if (listener.handleBeforeExecCommand) {
@ -51,6 +43,15 @@ export default function useEventListener(listener: UseEventListenerProps) {
}
});
minder.on(
'viewchange',
debounce((e: MinderEvent) => {
if (listener.handleViewChange) {
listener.handleViewChange(e);
}
}, 300)
);
// 监听脑图自定义事件
watch(
() => minderStore.event.timestamp,

View File

@ -101,8 +101,8 @@ export default {
hotboxMenu: {
expand: 'Expand/Collapse',
insetParent: 'Insert one level up',
insetSon: 'Insert next level',
insetBrother: 'Insert sibling',
insetSon: 'Add next level',
insetBrother: 'Add sibling',
copy: 'Copy',
cut: 'Cut',
paste: 'Paste',
@ -110,5 +110,6 @@ export default {
enterNode: 'Enter the current node',
},
loading: 'Mind map loading...',
unSavedEnterNodeTip: 'There are currently unsaved changes, please save before entering the node',
},
};

View File

@ -95,8 +95,8 @@ export default {
hotboxMenu: {
expand: '展开/收起',
insetParent: '插入上一级',
insetSon: '插入下一级',
insetBrother: '插入同级',
insetSon: '添加子级',
insetBrother: '添加同级',
copy: '复制',
cut: '剪切',
paste: '粘贴',
@ -104,5 +104,6 @@ export default {
enterNode: '进入当前节点',
},
loading: '脑图加载中...',
unSavedEnterNodeTip: '当前有未保存的改动,请先保存后再进入节点',
},
};

View File

@ -1,11 +1,11 @@
<template>
<!-- <div class="ms-minder-editor-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-divider v-if="props.iconButtons?.length" direction="vertical" :margin="0"></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)]" />
@ -14,89 +14,68 @@
<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"
/>
<a-button
type="outline"
:disabled="props.disabled"
class="px-[8px] py-[2px] text-[12px]"
size="small"
@click="save"
>
{{ t('minder.main.main.save') }}
</a-button>
</div>
</template>
<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 MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import { delProps, editMenuProps, insertProps, moleProps, priorityProps, tagProps, viewMenuProps } from '../props';
import useFullScreen from '@/hooks/useFullScreen';
import { useI18n } from '@/hooks/useI18n';
const props = defineProps({
...editMenuProps,
...insertProps,
...moleProps,
...priorityProps,
...tagProps,
...delProps,
...viewMenuProps,
minder: null,
import { MinderIconButtonItem } from '../props';
const props = defineProps<{
iconButtons?: MinderIconButtonItem[];
disabled?: boolean;
}>();
const emit = defineEmits<{
(e: 'click', eventTag: string): void;
(e: 'save'): void;
}>();
const { t } = useI18n();
const containerRef = ref<Element | null>(null);
const { toggleFullScreen, isFullScreen } = useFullScreen(containerRef);
onMounted(() => {
containerRef.value = document.querySelector('.ms-minder-editor-container');
});
// 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'));
function save() {
emit('save');
}
</script>
<style lang="less">
@import '../style/header.less';
.mind_tab-content {
.tab-icons {
background-image: url('@/assets/images/minder/icons.png');
background-repeat: no-repeat;
.ms-minder-editor-header {
@apply absolute z-10 flex items-center bg-white;
top: 16px;
right: 4px;
gap: 8px;
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 {
@apply !mr-0;
&:hover {
background-color: rgb(var(--primary-1)) !important;
.arco-icon {
color: rgb(var(--primary-4)) !important;
}
}
}
}
// .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,68 +1,7 @@
<template>
<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>
<div ref="mec" class="ms-minder-container">
<minderHeader :icon-buttons="props.iconButtons" @save="save" />
<Navigator />
<a-dropdown
v-model:popup-visible="menuVisible"
class="minder-dropdown"
position="bl"
:popup-translate="menuPopupOffset"
@select="handleMinderMenuSelect"
>
<span></span>
<template #content>
<a-doption value="expand">
<div class="flex items-center">
<div>{{ t('minder.hotboxMenu.expand') }}</div>
<div class="ml-[4px] text-[var(--color-text-4)]">( / )</div>
</div>
</a-doption>
<a-doption value="insetSon">
<div class="flex items-center">
<div>{{ t('minder.hotboxMenu.insetSon') }}</div>
<div class="ml-[4px] text-[var(--color-text-4)]">(Tab)</div>
</div>
</a-doption>
<a-doption value="insetBrother">
<div class="flex items-center">
<div>{{ t('minder.hotboxMenu.insetBrother') }}</div>
<div class="ml-[4px] text-[var(--color-text-4)]">(Enter)</div>
</div>
</a-doption>
<a-doption value="copy">
<div class="flex items-center">
<div>{{ t('minder.hotboxMenu.copy') }}</div>
<div class="ml-[4px] text-[var(--color-text-4)]">(Ctrl + C)</div>
</div>
</a-doption>
<a-doption value="cut">
<div class="flex items-center">
<div>{{ t('minder.hotboxMenu.cut') }}</div>
<div class="ml-[4px] text-[var(--color-text-4)]">(Ctrl + X)</div>
</div>
</a-doption>
<a-doption value="paste">
<div class="flex items-center">
<div>{{ t('minder.hotboxMenu.paste') }}</div>
<div class="ml-[4px] text-[var(--color-text-4)]">(Ctrl + V)</div>
</div>
</a-doption>
<a-doption value="delete">
<div class="flex items-center">
<div>{{ t('minder.hotboxMenu.delete') }}</div>
<div class="ml-[4px] text-[var(--color-text-4)]">(Backspace)</div>
</div>
</a-doption>
<a-doption value="enterNode">
<div class="flex items-center">
<div>{{ t('minder.hotboxMenu.enterNode') }}</div>
<div class="ml-[4px] text-[var(--color-text-4)]">(Ctrl+ Enter)</div>
</div>
</a-doption>
</template>
</a-dropdown>
<div
v-if="innerImportJson.treePath?.length > 1"
class="absolute left-[50%] top-[24px] z-50 translate-x-[-50%] bg-white p-[8px]"
@ -73,23 +12,30 @@
</a-breadcrumb-item>
</a-breadcrumb>
</div>
<nodeFloatMenu v-bind="props">
<template #extractMenu>
<slot name="extractMenu"></slot>
</template>
</nodeFloatMenu>
</div>
</template>
<script lang="ts" name="minderContainer" setup>
import { onMounted, ref, watch } from 'vue';
import { cloneDeep } from 'lodash-es';
import nodeFloatMenu from '../menu/nodeFloatMenu.vue';
import minderHeader from './header.vue';
import Navigator from './navigator.vue';
import { useI18n } from '@/hooks/useI18n';
import useMinderStore from '@/store/modules/components/minder-editor';
import { findNodePathByKey, getGenerateId } from '@/utils';
import { findNodePathByKey, replaceNodeInTree } from '@/utils';
import { MinderEventName } from '@/enums/minderEnum';
import {
editMenuProps,
floatMenuProps,
headerProps,
insertProps,
mainEditorProps,
MinderJson,
@ -102,21 +48,32 @@
import { markChangeNode, markDeleteNode } from '../script/tool/utils';
import type { Ref } from 'vue';
const { t } = useI18n();
const props = defineProps({ ...editMenuProps, ...insertProps, ...mainEditorProps, ...tagProps, ...priorityProps });
const props = defineProps({
...headerProps,
...floatMenuProps,
...editMenuProps,
...insertProps,
...mainEditorProps,
...tagProps,
...priorityProps,
});
const emit = defineEmits<{
(e: 'save', data: MinderJson, callback: () => void): void;
(e: 'afterMount'): void;
(e: 'save', json: MinderJson): void;
}>();
const minderStore = useMinderStore();
const mec: Ref<HTMLDivElement | null> = ref(null);
const innerImportJson = ref<any>({});
const importJson = defineModel<MinderJson>('importJson', {
required: true,
});
const innerImportJson = ref<MinderJson>({
root: {},
template: 'default',
treePath: [],
});
const minderUnsaved = ref(false);
function save() {
emit('save', window.minder.exportJson());
}
function handlePriorityButton() {
const { priorityPrefix } = props;
const { priorityStartWithZero } = props;
@ -162,8 +119,8 @@
moveEnable: props.moveEnable,
});
const { editor } = window;
if (Object.keys(props.importJson || {}).length > 0) {
editor.minder.importJson(props.importJson);
if (Object.keys(importJson.value || {}).length > 0) {
editor.minder.importJson(importJson.value);
}
window.km = editor.minder;
window.minder = window.km;
@ -189,11 +146,10 @@
'zoom',
'zoomIn',
'zoomOut',
'append',
'appendchildnode',
'appendsiblingnode',
]);
if (selectNodes && !notChangeCommands.has(env.commandName.toLocaleLowerCase())) {
minderUnsaved.value = true;
minderStore.dispatchEvent(MinderEventName.MINDER_CHANGED);
selectNodes.forEach((node: MinderJsonNode) => {
markChangeNode(node);
});
@ -223,15 +179,25 @@
* @param node 切换的节点
*/
function switchNode(node: MinderJsonNode | MinderJsonNodeData) {
if (node.data) {
innerImportJson.value = cloneDeep(findNodePathByKey([props.importJson.root], node.data.id, 'data', 'id'));
} else {
innerImportJson.value = cloneDeep(findNodePathByKey([props.importJson.root], node.id, 'data', 'id'));
if (minderUnsaved.value) {
//
replaceNodeInTree(
[importJson.value.root],
innerImportJson.value.root.data?.id || '',
window.minder.exportJson()?.root,
'data',
'id'
);
}
if (node.data) {
innerImportJson.value = findNodePathByKey([importJson.value.root], node.data.id, 'data', 'id') as MinderJson;
} else {
innerImportJson.value = findNodePathByKey([importJson.value.root], node.id, 'data', 'id') as MinderJson;
}
innerImportJson.value.data.expandState = 'expand';
window.minder.importJson(innerImportJson.value);
setTimeout(() => {
window.minder.execCommand('camera', window.minder.getRoot(), 600);
window.minder.select(window.minder.getRoot());
window.minder.execCommand('camera', window.minder.getRoot());
}, 100); // TODO:
}
@ -252,83 +218,23 @@
}
);
/**
* 执行插入
* @param command 插入命令
*/
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);
nextTick(() => {
const newNode: MinderJsonNode = window.minder.getSelectedNode();
if (!newNode.data) {
newNode.data = {
id: getGenerateId(),
text: '',
};
}
newNode.data.isNew = true; //
});
}
}
/**
* 处理快捷菜单选择
* @param val 选择的菜单项
*/
function handleMinderMenuSelect(val: string | number | Record<string, any> | undefined) {
const selectedNodes: MinderJsonNode[] = window.minder.getSelectedNodes();
if (selectedNodes.length > 0) {
switch (val) {
case 'expand':
if (selectedNodes.some((node) => node.data?.expandState === 'collapse')) {
window.minder.execCommand('Expand');
} else {
window.minder.execCommand('Collapse');
}
minderStore.dispatchEvent(MinderEventName.EXPAND, undefined, undefined, selectedNodes);
break;
case 'insetParent':
execInsertCommand('AppendParentNode');
minderStore.dispatchEvent(MinderEventName.INSERT_PARENT, undefined, undefined, selectedNodes);
break;
case 'insetSon':
execInsertCommand('AppendChildNode');
minderStore.dispatchEvent(MinderEventName.INSERT_CHILD, undefined, undefined, selectedNodes);
break;
case 'insetBrother':
execInsertCommand('AppendSiblingNode');
minderStore.dispatchEvent(MinderEventName.INSERT_SIBLING, undefined, undefined, selectedNodes);
break;
case 'copy':
window.minder.execCommand('Copy');
minderStore.dispatchEvent(MinderEventName.COPY_NODE, undefined, undefined, selectedNodes);
break;
case 'cut':
window.minder.execCommand('Cut');
minderStore.dispatchEvent(MinderEventName.CUT_NODE, undefined, undefined, selectedNodes);
break;
case 'paste':
window.minder.execCommand('Paste');
minderStore.dispatchEvent(MinderEventName.PASTE_NODE, undefined, undefined, selectedNodes);
break;
case 'delete':
window.minder.execCommand('RemoveNode');
minderStore.dispatchEvent(MinderEventName.DELETE_NODE, undefined, undefined, selectedNodes);
break;
case 'enterNode':
switchNode(selectedNodes[0]);
minderStore.dispatchEvent(MinderEventName.ENTER_NODE, undefined, undefined, [selectedNodes[0]]);
break;
default:
break;
}
function save() {
let data = importJson.value;
if (innerImportJson.value.treePath?.length > 1) {
replaceNodeInTree(
[importJson.value.root],
innerImportJson.value.root.data?.id || '',
window.minder.exportJson()?.root,
'data',
'id'
);
} else {
data = window.minder.exportJson();
}
emit('save', data, () => {
minderUnsaved.value = false;
menuVisible.value = false;
});
}
</script>
@ -337,12 +243,13 @@
.save-btn {
@apply !absolute;
}
.minder-container {
@apply relative !bg-white;
.ms-minder-container {
@apply relative overflow-hidden !bg-white;
padding: 16px 0;
height: calc(100% - 60px);
}
.minder-dropdown {
.ms-minder-dropdown {
.arco-dropdown-list-wrapper {
max-height: none;
}

View File

@ -60,18 +60,13 @@
return;
}
const nodes: MinderJsonNode[] = minder.getSelectedNodes();
let position: MinderNodePosition | undefined;
if (nodes.length > 0) {
if (props.delConfirm) {
props.delConfirm(nodes);
return;
}
const box = nodes[0].getRenderBox();
position = {
x: box.cx,
y: box.cy,
};
minderStore.dispatchEvent(MinderEventName.DELETE_NODE, position, nodes[0].rc.node, nodes);
minderStore.dispatchEvent(MinderEventName.DELETE_NODE, undefined, box, nodes[0].rc.node, nodes);
}
minder.forceRemoveNode();
}

View File

@ -99,7 +99,7 @@
}
window.minder.execCommand('resource', origin);
const nodes: MinderJsonNode[] = window.minder.getSelectedNodes();
minderStore.dispatchEvent(MinderEventName.SET_TAG, undefined, undefined, nodes);
minderStore.dispatchEvent(MinderEventName.SET_TAG, undefined, undefined, undefined, nodes);
if (props.replaceableTags) {
tagList.value = props.replaceableTags(nodes);
}

View File

@ -0,0 +1,420 @@
<template>
<a-trigger
v-model:popup-visible="menuVisible"
class="ms-minder-node-float-menu"
position="bl"
:popup-translate="menuPopupOffset"
trigger="click"
:click-outside-to-close="false"
popup-container=".ms-minder-container"
>
<span></span>
<template #content>
<a-radio-group
v-if="currentNodeTags.length > 0 && tags.length > 0"
v-model:model-value="currentNodeTags[0]"
type="button"
size="mini"
@change="(val) => handleTagChange(val as string)"
>
<a-radio v-for="tag of currentNodeTags" :key="tag" :value="tag">{{ tag }}</a-radio>
<a-radio v-for="tag of tags" :key="tag" :value="tag">{{ tag }}</a-radio>
</a-radio-group>
<a-dropdown
v-if="props.insertSiblingMenus.length > 0"
v-model:popup-visible="insertSiblingMenuVisible"
class="ms-minder-dropdown"
:popup-translate="[0, 4]"
position="bl"
trigger="click"
@select="(val) => handleMinderMenuSelect('AppendSiblingNode',val as string)"
>
<a-tooltip :content="t('minder.hotboxMenu.insetBrother')">
<MsButton
type="icon"
class="ms-minder-node-float-menu-icon-button"
:class="[insertSiblingMenuVisible ? 'ms-minder-node-float-menu-icon-button--focus' : '']"
>
<MsIcon type="icon-icon_title-top-align_outlined1" class="text-[var(--color-text-4)]" />
</MsButton>
</a-tooltip>
<template #content>
<div class="mx-[6px] px-[8px] py-[3px] text-[var(--color-text-4)]">
{{ t('minder.hotboxMenu.insetBrother') }}
</div>
<a-doption v-for="menu of props.insertSiblingMenus" :key="menu.value" :value="menu.value">
{{ t(menu.label) }}
</a-doption>
</template>
</a-dropdown>
<a-dropdown
v-if="props.insertSonMenus.length > 0"
v-model:popup-visible="insertSonMenuVisible"
class="ms-minder-dropdown"
:popup-translate="[0, 4]"
position="bl"
trigger="click"
@select="(val) => handleMinderMenuSelect('AppendChildNode',val as string)"
>
<a-tooltip :content="t('minder.hotboxMenu.insetSon')">
<MsButton
type="icon"
class="ms-minder-node-float-menu-icon-button"
:class="[insertSonMenuVisible ? 'ms-minder-node-float-menu-icon-button--focus' : '']"
>
<MsIcon type="icon-icon_title-left-align_outlined1" class="text-[var(--color-text-4)]" />
</MsButton>
</a-tooltip>
<template #content>
<div class="mx-[6px] px-[8px] py-[3px] text-[var(--color-text-4)]">
{{ t('minder.hotboxMenu.insetSon') }}
</div>
<a-doption v-for="menu of props.insertSonMenus" :key="menu.value" :value="menu.value">
{{ t(menu.label) }}
</a-doption>
</template>
</a-dropdown>
<a-dropdown
v-if="props.priorityCount && props.canShowPriorityMenu"
v-model:popup-visible="priorityMenuVisible"
class="ms-minder-dropdown"
:popup-translate="[0, 4]"
position="bl"
trigger="click"
@select="(val) => handleMinderMenuSelect('priority',val as string)"
>
<a-tooltip :content="props.priorityTooltip" :disabled="!props.priorityTooltip">
<MsButton
type="icon"
class="ms-minder-node-float-menu-icon-button"
:class="[priorityMenuVisible ? 'ms-minder-node-float-menu-icon-button--focus' : '']"
>
<div
class="h-[16px] w-[16px] rounded-full bg-[rgb(var(--primary-5))] text-center text-[12px] font-medium text-white"
>
P
</div>
</MsButton>
</a-tooltip>
<template #content>
<div v-if="props.priorityTooltip" class="mx-[6px] px-[8px] py-[3px] text-[var(--color-text-4)]">
{{ props.priorityTooltip }}
</div>
<template v-for="(item, pIndex) in priorityCount + 1" :key="item">
<a-doption v-if="pIndex != 0" :value="pIndex">
<div
class="flex h-[20px] w-[20px] items-center justify-center rounded-full text-[12px] font-medium text-white"
:style="{
backgroundColor: priorityColorMap[pIndex],
}"
>
{{ priorityPrefix }}{{ priorityStartWithZero ? pIndex - 1 : pIndex }}
</div>
</a-doption>
</template>
</template>
</a-dropdown>
<slot name="extractMenu"></slot>
<a-dropdown
v-if="props.canShowMoreMenu"
v-model:popup-visible="moreMenuVisible"
class="ms-minder-dropdown"
:popup-translate="[0, 4]"
position="bl"
trigger="click"
@select="(val) => handleMinderMenuSelect(val)"
>
<a-tooltip :content="t('common.more')">
<MsButton
type="icon"
class="ms-minder-node-float-menu-icon-button"
:class="[moreMenuVisible ? 'ms-minder-node-float-menu-icon-button--focus' : '']"
>
<MsIcon type="icon-icon_more_outlined" class="text-[var(--color-text-4)]" />
</MsButton>
</a-tooltip>
<template #content>
<a-doption v-if="props.canShowEnterNode" value="enterNode">
<div class="flex items-center">
<div>{{ t('minder.hotboxMenu.enterNode') }}</div>
<!-- <div class="ml-[4px] text-[var(--color-text-4)]">(Ctrl+ Enter)</div> -->
</div>
</a-doption>
<a-doption value="copy">
<div class="flex items-center">
<div>{{ t('minder.hotboxMenu.copy') }}</div>
<!-- <div class="ml-[4px] text-[var(--color-text-4)]">(Ctrl + C)</div> -->
</div>
</a-doption>
<a-doption value="cut">
<div class="flex items-center">
<div>{{ t('minder.hotboxMenu.cut') }}</div>
<!-- <div class="ml-[4px] text-[var(--color-text-4)]">(Ctrl + X)</div> -->
</div>
</a-doption>
<a-doption v-if="props.canShowPasteMenu && minderStore.clipboard.length > 0" value="paste">
<div class="flex items-center">
<div>{{ t('minder.hotboxMenu.paste') }}</div>
<!-- <div class="ml-[4px] text-[var(--color-text-4)]">(Ctrl + V)</div> -->
</div>
</a-doption>
<a-doption value="delete">
<div class="flex items-center">
<div>{{ t('minder.hotboxMenu.delete') }}</div>
<!-- <div class="ml-[4px] text-[var(--color-text-4)]">(Backspace)</div> -->
</div>
</a-doption>
</template>
</a-dropdown>
</template>
</a-trigger>
</template>
<script setup lang="ts">
import { TriggerPopupTranslate } 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 { useI18n } from '@/hooks/useI18n';
import useMinderStore from '@/store/modules/components/minder-editor/index';
import { MinderNodePosition } from '@/store/modules/components/minder-editor/types';
import { getGenerateId } from '@/utils';
import { MinderEventName } from '@/enums/minderEnum';
import { floatMenuProps, insertProps, MinderJsonNode, priorityProps, tagProps } from '../props';
import { isDisableNode, isNodeInMinderView, setPriorityView } from '../script/tool/utils';
const props = defineProps({
...floatMenuProps,
...insertProps,
...tagProps,
...priorityProps,
});
const { t } = useI18n();
const minderStore = useMinderStore();
const currentNodeTags = ref<string[]>([]);
const tags = ref<string[]>([]);
const menuVisible = ref(false);
const menuPopupOffset = ref<TriggerPopupTranslate>([0, 0]);
watch(
() => minderStore.event.timestamp,
() => {
let nodePosition: MinderNodePosition | undefined;
const selectedNodes: MinderJsonNode[] = window.minder.getSelectedNodes();
if (minderStore.event.name === MinderEventName.NODE_SELECT) {
nodePosition = minderStore.event.nodePosition;
currentNodeTags.value = minderStore.event.nodes?.[0].data?.resource || [];
if (props.replaceableTags) {
tags.value = props.replaceableTags(selectedNodes);
} else {
tags.value = [];
}
}
if (selectedNodes.length > 1) {
// TODO:
menuVisible.value = false;
return;
}
if (minderStore.event.name === MinderEventName.VIEW_CHANGE) {
//
nodePosition = window.minder.getSelectedNode()?.getRenderBox();
}
if (nodePosition && isNodeInMinderView(undefined, nodePosition, nodePosition.width / 2)) {
//
const nodeDomHeight = nodePosition.height || 0;
menuPopupOffset.value = [nodePosition.x, nodePosition.y + nodeDomHeight + 4]; // 4px
menuVisible.value = true;
} else {
menuVisible.value = false;
}
}
);
const insertSiblingMenuVisible = ref(false);
const insertSonMenuVisible = ref(false);
const priorityMenuVisible = ref(false);
const moreMenuVisible = ref(false);
/**
* 执行插入
* @param command 插入命令
*/
function execInsertCommand(command: string, value?: string) {
const node: MinderJsonNode = window.minder.getSelectedNode();
if (props.insertNode) {
props.insertNode(node, command, value);
return;
}
if (window.minder.queryCommandState(command) !== -1) {
window.minder.execCommand(command);
nextTick(() => {
const newNode: MinderJsonNode = window.minder.getSelectedNode();
if (!newNode.data) {
newNode.data = {
id: getGenerateId(),
text: '',
};
}
newNode.data.isNew = true; //
});
}
}
/**
* 切换标签
* @param value 切换后的标签
*/
function handleTagChange(value: string) {
const selectedNodes: MinderJsonNode[] = window.minder.getSelectedNodes();
if (selectedNodes.length > 0) {
const origin = window.minder.queryCommandValue('resource');
if (props.singleTag) {
origin.splice(0, origin.length, value);
} else {
const index = origin.indexOf(value);
//
if (props.distinctTags.indexOf(value) > -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(value);
}
}
window.minder.execCommand('resource', origin);
minderStore.dispatchEvent(MinderEventName.SET_TAG, undefined, undefined, undefined, selectedNodes);
}
}
const priorityColorMap: Record<number, string> = {
1: 'rgb(var(--danger-6))',
2: 'rgb(var(--link-6))',
3: 'rgb(var(--success-6))',
4: 'rgb(var(--warning-6))',
};
const priorityDisabled = ref(true);
function isDisable(): boolean {
if (Object.keys(window.minder).length === 0) return true;
nextTick(() => {
setPriorityView(props.priorityStartWithZero, props.priorityPrefix);
});
const node = window.minder.getSelectedNode();
if (isDisableNode(window.minder) || !node || node.parent === null) {
return true;
}
if (props.priorityDisableCheck) {
return props.priorityDisableCheck(node);
}
return !!window.minder.queryCommandState && window.minder.queryCommandState('priority') === -1;
}
/**
* 处理快捷菜单选择
* @param type 选择的菜单项
*/
function handleMinderMenuSelect(type: string | number | Record<string, any> | undefined, value?: string) {
const selectedNodes: MinderJsonNode[] = window.minder.getSelectedNodes();
if (selectedNodes.length > 0) {
switch (type) {
case 'AppendChildNode':
execInsertCommand('AppendChildNode', value);
minderStore.dispatchEvent(MinderEventName.INSERT_CHILD, value, undefined, undefined, selectedNodes);
break;
case 'AppendSiblingNode':
execInsertCommand('AppendSiblingNode', value);
minderStore.dispatchEvent(MinderEventName.INSERT_SIBLING, value, undefined, undefined, selectedNodes);
break;
case 'copy':
minderStore.dispatchEvent(MinderEventName.COPY_NODE, undefined, undefined, undefined, selectedNodes);
window.minder.execCommand('Copy');
break;
case 'cut':
minderStore.dispatchEvent(MinderEventName.CUT_NODE, undefined, undefined, undefined, selectedNodes);
window.minder.execCommand('Cut');
break;
case 'paste':
minderStore.dispatchEvent(MinderEventName.PASTE_NODE, undefined, undefined, undefined, selectedNodes);
window.minder.execCommand('Paste');
break;
case 'delete':
minderStore.dispatchEvent(MinderEventName.DELETE_NODE, undefined, undefined, undefined, selectedNodes);
window.minder.execCommand('RemoveNode');
break;
case 'enterNode':
minderStore.dispatchEvent(MinderEventName.ENTER_NODE, undefined, undefined, undefined, [selectedNodes[0]]);
break;
case 'priority':
if (value && !priorityDisabled.value) {
window.minder.execCommand('priority', value);
setPriorityView(props.priorityStartWithZero, props.priorityPrefix);
} else if (window.minder.execCommand && !priorityDisabled.value) {
window.minder.execCommand('priority');
}
break;
default:
break;
}
}
//
nextTick(() => {
menuVisible.value = true;
});
}
onMounted(() => {
nextTick(() => {
const freshFuc = setPriorityView;
if (window.minder) {
window.minder.on('contentchange', () => {
//
setTimeout(() => {
freshFuc(props.priorityStartWithZero, props.priorityPrefix);
}, 0);
});
window.minder.on('selectionchange', () => {
priorityDisabled.value = isDisable();
});
}
});
});
</script>
<style lang="less">
.ms-minder-node-float-menu {
.arco-trigger-content {
@apply flex w-auto flex-1 items-center bg-white;
padding: 4px 8px;
gap: 8px;
border-radius: var(--border-radius-small);
box-shadow: 0 4px 10px -1px rgb(100 100 102 / 15%);
}
.ms-minder-node-float-menu-icon-button {
@apply !mr-0;
&:hover {
background-color: rgb(var(--primary-1)) !important;
.arco-icon {
color: rgb(var(--primary-4)) !important;
}
}
}
.ms-minder-node-float-menu-icon-button--focus {
background-color: rgb(var(--primary-1)) !important;
.arco-icon {
color: rgb(var(--primary-5)) !important;
}
}
}
</style>

View File

@ -1,53 +1,11 @@
<template>
<a-spin :loading="loading" class="ms-minder-editor-container">
<div class="flex-1">
<minderHeader
:sequence-enable="props.sequenceEnable"
:tag-enable="props.tagEnable"
:progress-enable="props.progressEnable"
:priority-count="props.priorityCount"
:priority-prefix="props.priorityPrefix"
:priority-start-with-zero="props.priorityStartWithZero"
:tags="props.tags"
:move-enable="props.moveEnable"
:move-confirm="props.moveConfirm"
:tag-edit-check="props.tagEditCheck"
:tag-disable-check="props.tagDisableCheck"
:priority-disable-check="props.priorityDisableCheck"
:distinct-tags="props.distinctTags"
:default-mold="props.defaultMold"
:del-confirm="props.delConfirm"
:arrange-enable="props.arrangeEnable"
: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"
/>
<mainEditor
:disabled="props.disabled"
:sequence-enable="props.sequenceEnable"
:tag-enable="props.tagEnable"
:move-enable="props.moveEnable"
:move-confirm="props.moveConfirm"
:progress-enable="props.progressEnable"
:import-json="props.importJson"
:height="props.height"
:tags="props.tags"
:distinct-tags="props.distinctTags"
:tag-edit-check="props.tagEditCheck"
:tag-disable-check="props.tagDisableCheck"
: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"
/>
<mainEditor v-model:import-json="importJson" v-bind="props" @after-mount="() => emit('afterMount')" @save="save">
<template #extractMenu>
<slot name="extractMenu"></slot>
</template>
</mainEditor>
</div>
<template v-if="props.extractContentTabList?.length">
<div class="ms-minder-editor-extra" :class="[extraVisible ? 'ms-minder-editor-extra--visible' : '']">
@ -64,19 +22,22 @@
<script lang="ts" name="minderEditor" setup>
import MsTab from '@/components/pure/ms-tab/index.vue';
import minderHeader from './main/header.vue';
import mainEditor from './main/mainEditor.vue';
import useMinderStore from '@/store/modules/components/minder-editor/index';
import { MinderCustomEvent } from '@/store/modules/components/minder-editor/types';
import { MinderEventName } from '@/enums/minderEnum';
import useMinderEventListener from './hooks/useMinderEventListener';
import {
delProps,
editMenuProps,
headerProps,
floatMenuProps,
insertProps,
mainEditorProps,
MinderEvent,
MinderJson,
MinderJsonNode,
moleProps,
priorityProps,
@ -86,17 +47,17 @@
const emit = defineEmits<{
(e: 'moldChange', data: number): void;
(e: 'save', data: Record<string, any>): void;
(e: 'save', data: MinderJson, callback: () => void): void;
(e: 'afterMount'): void;
(e: 'enterNode', data: MinderJsonNode): void;
(e: 'nodeSelect', data: MinderJsonNode): void;
(e: 'contentChange', data: MinderJsonNode): void;
(e: 'action', event: MinderCustomEvent): void;
(e: 'beforeExecCommand', event: MinderEvent): void;
(e: 'nodeUnselect'): void;
}>();
const props = defineProps({
...headerProps,
...floatMenuProps,
...insertProps,
...editMenuProps,
...mainEditorProps,
@ -116,29 +77,43 @@
const extraVisible = defineModel<boolean>('extraVisible', {
default: false,
});
const importJson = defineModel<MinderJson>('importJson', {
default: () => ({
root: {},
template: 'default',
treePath: [] as any[],
}),
});
const minderStore = useMinderStore();
onMounted(async () => {
window.minderProps = props;
});
function handleMoldChange(data: number) {
emit('moldChange', data);
}
function save(data: Record<string, any>) {
emit('save', data);
}
function handleEnterNode(data: any) {
emit('enterNode', data);
function save(data: MinderJson, callback: () => void) {
emit('save', data, callback);
}
onMounted(() => {
useMinderEventListener({
handleSelectionChange: () => {
const selectedNode: MinderJsonNode = window.minder.getSelectedNode();
if (Object.keys(window.minder).length > 0 && selectedNode) {
if (selectedNode) {
emit('nodeSelect', selectedNode);
const box = selectedNode.getRenderBox();
minderStore.dispatchEvent(
MinderEventName.NODE_SELECT,
undefined,
{
...box,
},
selectedNode.rc.node,
[selectedNode]
);
} else {
emit('nodeUnselect');
minderStore.dispatchEvent(MinderEventName.NODE_UNSELECT);
}
},
handleContentChange: (node: MinderJsonNode) => {
@ -150,6 +125,9 @@
handleBeforeExecCommand: (event) => {
emit('beforeExecCommand', event);
},
handleViewChange() {
minderStore.dispatchEvent(MinderEventName.VIEW_CHANGE);
},
});
});
</script>
@ -158,10 +136,9 @@
.ms-minder-editor-container {
@apply relative flex h-full w-full;
.ms-minder-editor-extra {
@apply flex flex-col overflow-hidden border-l;
@apply flex flex-col overflow-hidden;
width: 0;
border-color: var(--color-text-n8);
transition: all 300ms ease-in-out;
.ms-minder-editor-extra-content {
@apply relative flex-1 overflow-y-auto;
@ -171,8 +148,11 @@
}
}
.ms-minder-editor-extra--visible {
@apply border-l;
width: 35%;
min-width: 360px;
border-color: var(--color-text-n8);
transition: all 300ms ease-in-out;
animation: minWidth 300ms ease-in-out;
}

View File

@ -29,13 +29,14 @@ export interface MinderJsonNode {
data?: MinderJsonNodeData;
parent?: MinderJsonNode;
children?: MinderJsonNode[];
type?: string; // 节点类型root为根节点其他为普通节点
[key: string]: any; // minder 内置字段
}
export interface MinderJson {
root: MinderJsonNode;
template: string;
treePath: Record<string, MinderJsonNode>[];
treePath: MinderJsonNode[];
}
// 脑图类
export interface MinderClass {
@ -53,16 +54,6 @@ export interface MinderEvent extends MinderClass {
}
export const mainEditorProps = {
importJson: {
type: Object as PropType<MinderJson>,
default() {
return {
root: {},
template: 'default',
treePath: [] as any[],
};
},
},
height: {
type: Number,
default: 500,
@ -98,6 +89,10 @@ export const priorityProps = {
},
priorityDisableCheck: Function as PropType<(node: MinderJsonNode) => boolean>,
operators: [],
priorityTooltip: {
type: String,
default: '',
},
};
export interface MinderReplaceTag {
@ -130,9 +125,54 @@ export const tagProps = {
afterTagEdit: Function as PropType<(nodes: MinderJsonNode[], tag: string) => void>,
};
export interface InsertMenuItem {
value: string;
label: string;
}
export const floatMenuProps = {
// 插入同级选项
insertSiblingMenus: {
type: Array as PropType<InsertMenuItem[]>,
default() {
return [];
},
},
// 插入子级选项
insertSonMenus: {
type: Array as PropType<InsertMenuItem[]>,
default() {
return [];
},
},
// 是否显示更多菜单
canShowMoreMenu: {
type: Boolean,
default: true,
},
// 是否显示进入节点
canShowEnterNode: {
type: Boolean,
default: false,
},
// 是否显示粘贴菜单
canShowPasteMenu: {
type: Boolean,
default: true,
},
// 是否显示等级菜单
canShowPriorityMenu: {
type: Boolean,
default: true,
},
// 节点可选标签集合
replaceableTags: {
type: Function as PropType<(nodes: MinderJsonNode[]) => string[]>,
},
};
export const insertProps = {
insertNode: {
type: Function as PropType<(node: MinderJsonNode, type: string) => void>,
type: Function as PropType<(node: MinderJsonNode, type: string, value?: string) => void>,
default: undefined,
},
};

View File

@ -16,14 +16,9 @@ function HotboxRuntime(this: any) {
function handleHotBoxShow() {
const node = minder.getSelectedNode();
let position: MinderNodePosition | undefined;
if (node) {
const box = node.getRenderBox();
position = {
x: box.cx,
y: box.cy,
};
minderStore.dispatchEvent(MinderEventName.HOTBOX, position, node.rc.node);
minderStore.dispatchEvent(MinderEventName.HOTBOX, undefined, box, node.rc.node);
}
}

View File

@ -1,3 +1,5 @@
import type { MinderNodePosition } from '@/store/modules/components/minder-editor/types';
import type { MinderJsonNode } from '../../props';
export function isDisableNode(minder: any) {
@ -120,3 +122,18 @@ export function isDisableForNode(node: MinderJsonNode) {
}
return false;
}
/**
*
* @param node
*/
export function isNodeInMinderView(node?: MinderJsonNode, nodePosition?: MinderNodePosition, offsetX?: number) {
const containerWidth = document.querySelector('.ms-minder-container')?.clientWidth || 0;
if (node) {
nodePosition = (node || window.minder.getSelectedNode())?.getRenderBox();
}
if (nodePosition) {
return nodePosition.x >= 0 && nodePosition.x + (offsetX || 0) <= containerWidth;
}
return false;
}

View File

@ -82,6 +82,7 @@ export const reviewDefaultDetail: ReviewItem = {
unPassCount: 0,
reviewedCount: 0,
underReviewedCount: 0,
unReviewCount: 0,
pos: 5000,
startTime: 0,
endTime: 0,

View File

@ -3,13 +3,16 @@ export enum MinderEventName {
'HOTBOX' = 'HOTBOX', // 热键菜单
'ENTER_NODE' = 'ENTER_NODE', // 进入节点
'EXPAND' = 'EXPAND', // 展开节点
'INSERT_PARENT' = 'INSERT_PARENT', // 插入父节点
'INSERT_CHILD' = 'INSERT_CHILD', // 插入子节点
'INSERT_SIBLING' = 'INSERT_SIBLING', // 插入同级节点
'COPY_NODE' = 'COPY_NODE', // 复制节点
'PASTE_NODE' = 'PASTE_NODE', // 粘贴节点
'CUT_NODE' = 'CUT_NODE', // 剪切节点
'SET_TAG' = 'SET_TAG', // 设置节点标签
'NODE_SELECT' = 'NODE_SELECT', // 选中节点
'NODE_UNSELECT' = 'NODE_UNSELECT', // 取消选中节点
'VIEW_CHANGE' = 'VIEW_CHANGE', // 脑图视图移动
'MINDER_CHANGED' = 'MINDER_CHANGED', // 脑图更改事件
}
export default {};

View File

@ -11,7 +11,7 @@ export interface UseFullScreen {
* @param domRef dom ref
*/
export default function useFullScreen(
domRef: Ref<HTMLElement | null | undefined> | HTMLElement | Element | null | undefined
domRef: Ref<HTMLElement | Element | null | undefined> | HTMLElement | Element | null | undefined
): UseFullScreen {
const isFullScreen = ref(false);
const originalStyle = ref('');

View File

@ -178,6 +178,7 @@ export interface ReviewItem {
unPassCount: number;
reReviewedCount: number;
underReviewedCount: number;
unReviewCount: number;
reviewedCount: number;
followFlag: boolean; // 关注标识
}

View File

@ -2,7 +2,7 @@ import { defineStore } from 'pinia';
import type { MinderJsonNode } from '@/components/pure/ms-minder-editor/props';
import type { MinderEventName } from '@/enums/minderEnum';
import { MinderEventName } from '@/enums/minderEnum';
import { MinderNodePosition, MinderState } from './types';
@ -12,14 +12,16 @@ const useMinderStore = defineStore('minder', {
event: {
name: '' as MinderEventName,
timestamp: 0,
params: '',
nodePosition: {
x: 0,
y: 0,
},
} as MinderNodePosition,
nodeDom: undefined,
nodes: undefined,
},
mold: 0,
clipboard: [],
}),
actions: {
/**
@ -31,21 +33,29 @@ const useMinderStore = defineStore('minder', {
*/
dispatchEvent(
name: MinderEventName,
params?: string,
position?: MinderNodePosition,
nodeDom?: HTMLElement,
nodes?: MinderJsonNode[]
) {
this.event = {
name,
params,
timestamp: Date.now(),
nodePosition: position,
nodeDom,
nodes,
};
if ([MinderEventName.COPY_NODE, MinderEventName.CUT_NODE].includes(name)) {
this.setClipboard(nodes);
}
},
setMold(val: number) {
this.mold = val;
},
setClipboard(nodes?: MinderJsonNode[]) {
this.clipboard = nodes || [];
},
},
});

View File

@ -5,11 +5,20 @@ import type { MinderEventName } from '@/enums/minderEnum';
export interface MinderNodePosition {
x: number;
y: number;
cx: number;
cy: number;
height: number;
left: number;
right: number;
top: number;
width: number;
bottom: number;
}
export interface MinderCustomEvent {
name: MinderEventName;
timestamp: number;
params?: any;
nodePosition?: MinderNodePosition;
nodeDom?: HTMLElement;
nodes?: MinderJsonNode[];
@ -18,4 +27,5 @@ export interface MinderCustomEvent {
export interface MinderState {
event: MinderCustomEvent;
mold: number;
clipboard: MinderJsonNode[]; // 剪切板
}

View File

@ -315,16 +315,17 @@ export function filterTree<T>(
export function findNodeByKey<T>(
trees: TreeNode<T>[],
targetKey: string | number,
customKey = 'key'
customKey = 'key',
dataKey: string | undefined = undefined
): TreeNode<T> | T | null {
for (let i = 0; i < trees.length; i++) {
const node = trees[i];
if (node[customKey] === targetKey) {
if (dataKey ? node[dataKey]?.[customKey] === targetKey : node[customKey] === targetKey) {
return node; // 如果当前节点的 key 与目标 key 匹配,则返回当前节点
}
if (Array.isArray(node.children) && node.children.length > 0) {
const _node = findNodeByKey(node.children, targetKey, customKey); // 递归在子节点中查找
const _node = findNodeByKey(node.children, targetKey, customKey, dataKey); // 递归在子节点中查找
if (_node) {
return _node; // 如果在子节点中找到了匹配的节点,则返回该节点
}
@ -389,6 +390,34 @@ export function findNodePathByKey<T>(
return null;
}
/**
* customKey替换树节点
*/
export function replaceNodeInTree<T>(
tree: TreeNode<T>[],
targetKey: string,
newNode: TreeNode<T>,
dataKey?: string,
customKey = 'key'
): boolean {
for (let i = 0; i < tree.length; i++) {
const node = tree[i];
if (dataKey ? node[dataKey]?.[customKey] === targetKey : node[customKey] === targetKey) {
// 找到目标节点,进行替换
tree[i] = newNode;
return true;
}
if (node.children && node.children.length > 0) {
// 如果当前节点有子节点,递归查找
if (replaceNodeInTree(node.children, targetKey, newNode, dataKey, customKey)) {
return true;
}
}
}
return false;
}
/**
* /
* @param treeArr

View File

@ -1452,15 +1452,12 @@
:deep(.param-input:not(.arco-input-focus, .arco-select-view-focus)) {
&:not(:hover) {
border-color: transparent !important;
.arco-input::placeholder {
@apply invisible;
}
.arco-select-view-icon {
@apply invisible;
}
.arco-select-view-value {
color: var(--color-text-brand);
}

View File

@ -1465,8 +1465,10 @@
watch(
() => showType.value,
() => {
initData();
(val) => {
if (val === 'list') {
initData();
}
}
);

View File

@ -141,7 +141,6 @@
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { Message } from '@arco-design/web-vue';
import { filter } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
@ -153,7 +152,6 @@
import MsTree from '@/components/business/ms-tree/index.vue';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import TableFilter from '../../tableFilter.vue';
import conditionStatus from '@/views/api-test/report/component/conditionStatus.vue';
import {
addPrepositionRelation,

View File

@ -47,6 +47,15 @@
{{ props.reviewDetail.underReviewedCount }}
</td>
</tr>
<tr>
<td class="popover-label-td">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[var(--color-text-4)]"></div>
<div>{{ t('caseManagement.caseReview.unReview') }}</div>
</td>
<td class="popover-value-td">
{{ props.reviewDetail.unReviewCount }}
</td>
</tr>
</table>
</template>
</MsColorLine>