feat(组件): 脑图支持新功能&部分组件调整

This commit is contained in:
baiqi 2023-11-10 10:23:44 +08:00 committed by Craftsman
parent 17215a686e
commit bff4a60c58
29 changed files with 588 additions and 194 deletions

View File

@ -90,5 +90,16 @@ export default {
priority: 'Priority',
tag: 'Tag',
},
hotboxMenu: {
expand: 'Expand/Collapse',
insetParent: 'Insert one level up',
insetSon: 'Insert next level',
insetBrother: 'Insert sibling',
copy: 'Copy',
cut: 'Cut',
paste: 'Paste',
delete: 'Delete',
enterNode: 'Enter the current node',
},
},
};

View File

@ -84,5 +84,16 @@ export default {
priority: '优先级',
tag: '标签',
},
hotboxMenu: {
expand: '展开/收起',
insetParent: '插入上一级',
insetSon: '插入下一级',
insetBrother: '插入同级',
copy: '复制',
cut: '剪切',
paste: '粘贴',
delete: '删除',
enterNode: '进入当前节点',
},
},
};

View File

@ -4,6 +4,7 @@
<edit-menu
:minder="minder"
:move-enable="props.moveEnable"
:move-confirm="props.moveConfirm"
:sequence-enable="props.sequenceEnable"
:tag-enable="props.tagEnable"
:progress-enable="props.progressEnable"

View File

@ -1,18 +1,96 @@
<template>
<div ref="mec" class="minder-container" :style="{ height: `${props.height}px` }">
<a-button type="primary" :disabled="props.disabled" class="save-btn bottom-[30px] right-[30px]" @click="save">{{
t('minder.main.main.save')
}}</a-button>
<a-button type="primary" :disabled="props.disabled" class="save-btn bottom-[30px] right-[30px]" @click="save">
{{ t('minder.main.main.save') }}
</a-button>
<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="insetParent">
<div class="flex items-center">
<div>{{ t('minder.hotboxMenu.insetParent') }}</div>
<div class="ml-[4px] text-[var(--color-text-4)]">(Shift + Tab)</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]"
>
<a-breadcrumb>
<a-breadcrumb-item v-for="crumb of innerImportJson.treePath" :key="crumb.name" @click="switchNode(crumb)">
{{ crumb.text }}
</a-breadcrumb-item>
</a-breadcrumb>
</div>
</div>
</template>
<script lang="ts" name="minderContainer" setup>
import { onMounted, ref } from 'vue';
import { onMounted, ref, watch } from 'vue';
import { cloneDeep } from 'lodash-es';
import Navigator from './navigator.vue';
import { useI18n } from '@/hooks/useI18n';
import useMinderStore from '@/store/modules/components/minder-editor';
import { findNodePathByKey } from '@/utils';
import { editMenuProps, mainEditorProps, priorityProps, tagProps } from '../props';
import Editor from '../script/editor';
@ -25,9 +103,12 @@
const emit = defineEmits({
afterMount: () => ({}),
save: (json) => json,
enterNode: (data) => data,
});
const minderStore = useMinderStore();
const mec: Ref<HTMLDivElement | null> = ref(null);
const innerImportJson = ref<any>({});
function save() {
emit('save', window.minder.exportJson());
@ -129,6 +210,80 @@
onMounted(async () => {
init();
});
watch(
() => props.importJson,
(val) => {
innerImportJson.value = val;
window.minder.importJson(val);
}
);
const menuVisible = ref(false);
const menuPopupOffset = ref([0, 0]);
function switchNode(node: any) {
innerImportJson.value = cloneDeep(findNodePathByKey([props.importJson.root], node.id, 'data', 'id'));
innerImportJson.value.data.expandState = 'expand';
window.minder.importJson(innerImportJson.value);
window.minder.execCommand('template', Object.keys(window.kityminder.Minder.getTemplateList())[minderStore.mold]);
}
watch(
() => minderStore.event.timestamp,
() => {
if (minderStore.event.name === 'hotbox') {
const nodeDomWidth = minderStore.event.nodeDom?.getBoundingClientRect().width || 0;
menuPopupOffset.value = [
minderStore.event.nodePosition.x + nodeDomWidth / 2,
minderStore.event.nodePosition.y - nodeDomWidth / 4,
];
menuVisible.value = true;
}
if (minderStore.event.name === 'enterNode') {
switchNode(minderStore.event.nodeData);
}
}
);
function handleMinderMenuSelect(val: string | number | Record<string, any> | undefined) {
const selectedNode = window.minder.getSelectedNode();
switch (val) {
case 'expand':
if (selectedNode.data.expandState === 'collapse') {
window.minder.execCommand('Expand');
} else {
window.minder.execCommand('Collapse');
}
break;
case 'insetParent':
window.minder.execCommand('AppendParentNode');
break;
case 'insetSon':
window.minder.execCommand('AppendChildNode');
break;
case 'insetBrother':
window.minder.execCommand('AppendSiblingNode');
break;
case 'copy':
window.minder.execCommand('Copy');
break;
case 'cut':
window.minder.execCommand('Cut');
break;
case 'paste':
window.minder.execCommand('Paste');
break;
case 'delete':
window.minder.execCommand('RemoveNode');
break;
case 'enterNode':
switchNode(selectedNode.data);
break;
default:
break;
}
}
</script>
<style lang="less">
@ -139,4 +294,9 @@
.minder-container {
@apply relative;
}
.minder-dropdown {
.arco-dropdown-list-wrapper {
max-height: none;
}
}
</style>

View File

@ -17,7 +17,7 @@
</a-button>
{{ t('minder.menu.expand.folding') }}
</div>
<move-box :move-enable="props.moveEnable" />
<move-box :move-enable="props.moveEnable" :move-confirm="props.moveConfirm" />
<insert-box />
<edit-del :del-confirm="props.delConfirm" />
</div>
@ -39,10 +39,14 @@
:priority-start-with-zero="props.priorityStartWithZero"
/>
</div>
<div class="menu-group">
<mold v-if="props.moldEnable" :default-mold="props.defaultMold" @mold-change="handleMoldChange" />
</div>
</div>
</template>
<script lang="ts" name="editMenu" setup>
import mold from '../view/mold.vue';
import editDel from './editDel.vue';
import insertBox from './insertBox.vue';
import moveBox from './moveBox.vue';
@ -51,9 +55,19 @@
import { useI18n } from '@/hooks/useI18n';
import { delProps, editMenuProps, priorityProps, tagProps } from '../../props';
import { delProps, editMenuProps, moleProps, priorityProps, tagProps, viewMenuProps } from '../../props';
const props = defineProps({ ...editMenuProps, ...priorityProps, ...tagProps, ...delProps });
const props = defineProps({
...editMenuProps,
...priorityProps,
...tagProps,
...delProps,
...viewMenuProps,
...moleProps,
});
const emit = defineEmits<{
(e: 'moldChange', data: number): void;
}>();
const { t } = useI18n();
@ -94,4 +108,8 @@
window.minder?.execCommand('ExpandToLevel', 1);
}
}
function handleMoldChange(data: number) {
emit('moldChange', data);
}
</script>

View File

@ -40,6 +40,7 @@
const props = defineProps<{
moveEnable: boolean;
moveConfirm: any;
}>();
let minder = reactive<any>({});

View File

@ -1,27 +1,34 @@
<template>
<div class="mold-group" :disabled="disabled">
<a-dropdown class="toggle" @select="handleCommand">
<div>
<span class="dropdown-toggle mold-icons menu-btn" :class="'mold-' + (moldIndex + 1)" />
<span class="dropdown-link">
<icon-caret-down />
</span>
</div>
<template #content>
<a-doption class="dropdown-item mold-icons mold-1" :value="1" />
<a-doption class="dropdown-item mold-icons mold-2" :value="2" />
<a-doption class="dropdown-item mold-icons mold-3" :value="3" />
<a-doption class="dropdown-item mold-icons mold-4" :value="4" />
<a-doption class="dropdown-item mold-icons mold-5" :value="5" />
<a-doption class="dropdown-item mold-icons mold-6" :value="6" />
</template>
</a-dropdown>
</div>
<a-dropdown class="toggle" :disabled="disabled" @select="handleCommand">
<span class="dropdown-toggle mold-icons menu-btn cursor-pointer" :class="'mold-' + (moldIndex + 1)" />
<template #content>
<a-doption class="dropdown-item" :value="1">
<div class="mold-icons mold-1"></div>
</a-doption>
<a-doption class="dropdown-item" :value="2">
<div class="mold-icons mold-2"></div>
</a-doption>
<a-doption class="dropdown-item" :value="3">
<div class="mold-icons mold-3"></div>
</a-doption>
<a-doption class="dropdown-item" :value="4">
<div class="mold-icons mold-4"></div>
</a-doption>
<a-doption class="dropdown-item" :value="5">
<div class="mold-icons mold-5"></div>
</a-doption>
<a-doption class="dropdown-item" :value="6">
<div class="mold-icons mold-6"></div>
</a-doption>
</template>
</a-dropdown>
</template>
<script lang="ts" name="Mold" setup>
import { computed, nextTick, onMounted, ref } from 'vue';
import useMinderStore from '@/store/modules/components/minder-editor';
import { moleProps } from '../../props';
const props = defineProps(moleProps);
@ -30,6 +37,7 @@
(e: 'moldChange', data: number): void;
}>();
const minderStore = useMinderStore();
const moldIndex = ref(0);
const disabled = computed(() => {
@ -45,9 +53,10 @@
const templateList = computed(() => window.kityminder.Minder.getTemplateList());
function handleCommand(value: string | number | Record<string, any> | undefined) {
moldIndex.value = value as number;
window.minder.execCommand('template', Object.keys(templateList.value)[value as number]);
emit('moldChange', value as number);
moldIndex.value = (value as number) - 1;
window.minder.execCommand('template', Object.keys(templateList.value)[(value as number) - 1]);
minderStore.setMold((value as number) - 1);
emit('moldChange', (value as number) - 1);
}
onMounted(() => {
@ -55,40 +64,19 @@
});
</script>
<style lang="less">
.toggle {
.arco-dropdown-list {
@apply grid grid-cols-2;
padding: 5px;
gap: 5px;
}
}
</style>
<style lang="less" scoped>
:deep(.arco-dropdown-list) {
@apply grid grid-cols-2;
}
.dropdown-toggle .mold-icons,
.mold-icons {
background-image: url('@/assets/images/minder/mold.png');
background-repeat: no-repeat;
}
.mold-group {
@apply relative flex items-center justify-center;
.dropdown-item {
@apply flex items-center justify-center;
width: 80px;
.dropdown-toggle {
@apply flex;
margin-top: 5px;
width: 50px;
height: 50px;
}
}
.dropdown-link {
@apply absolute cursor-pointer;
right: 3px;
bottom: 2px;
height: 50px !important;
}
.mold-loop(@i) when (@i > 0) {
.mold-@{i} {
@ -96,7 +84,7 @@
margin-top: 5px;
width: 50px;
height: 50px;
height: 45px;
background-position: (1 - @i) * 50px 0;
}
.mold-loop(@i - 1);

View File

@ -9,6 +9,7 @@
: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"
@ -26,6 +27,7 @@
: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"
@ -38,6 +40,7 @@
:priority-start-with-zero="props.priorityStartWithZero"
@after-mount="emit('afterMount')"
@save="save"
@enter-node="handleEnterNode"
/>
</div>
</template>
@ -54,6 +57,8 @@
(e: 'moldChange', data: number): void;
(e: 'save', data: Record<string, any>): void;
(e: 'afterMount'): void;
(e: 'enterNode', data: any): void;
(e: 'nodeClick', data: any): void;
}>();
const props = defineProps({
@ -73,7 +78,28 @@
function handleMoldChange(data: number) {
emit('moldChange', data);
}
function save(data: Record<string, any>) {
emit('save', data);
}
function handleEnterNode(data: any) {
emit('enterNode', data);
}
onMounted(() => {
nextTick(() => {
if (window.minder.on) {
window.minder.on('mousedown', (e: any) => {
if (e.originEvent.button === 0) {
//
const selectedNode = window.minder.getSelectedNode();
if (Object.keys(window.minder).length > 0 && selectedNode) {
emit('nodeClick', selectedNode.data);
}
}
});
}
});
});
</script>

View File

@ -7,25 +7,9 @@ export const mainEditorProps = {
type: Object,
default() {
return {
root: {
data: {
text: 'test111',
},
children: [
{
data: {
text: '地图',
},
},
{
data: {
text: '百科',
expandState: 'collapse',
},
},
],
},
root: {},
template: 'default',
treePath: [] as any[],
};
},
},
@ -95,6 +79,10 @@ export const editMenuProps = {
type: Boolean,
default: true,
},
moveConfirm: {
type: Function,
default: null,
},
};
export const moleProps = {

View File

@ -16,20 +16,20 @@ const MimeType = () => {
'\uFFFF': 'application/km',
};
function getSpitor(): string {
function getSplitor(): string {
return SPLITOR;
}
function isPureText(text: string): boolean {
// eslint-disable-next-line no-bitwise
return !~text.indexOf(getSpitor());
return !~text.indexOf(getSplitor());
}
function getPureText(text: string): string {
if (isPureText(text)) {
return text;
}
return text.split(getSpitor())[1];
return text.split(getSplitor())[1];
}
function getMimeType(sign?: string): MimeTypes | string | null {
@ -43,7 +43,7 @@ const MimeType = () => {
if (isPureText(text)) {
return null;
}
return getMimeType(text.split(getSpitor())[0]);
return getMimeType(text.split(getSplitor())[0]);
}
function process(mimetype: string | false, text: string): string {
@ -84,8 +84,12 @@ const MimeType = () => {
return {
registerMimeTypeProtocol,
getMimeTypeProtocol,
getSpitor,
getSplitor,
getMimeType,
whichMimeType,
process,
getPureText,
isPureText,
};
};

View File

@ -13,10 +13,6 @@ interface IData {
};
}
interface ICliboardEvent extends ClipboardEvent {
clipboardData: DataTransfer;
}
export default function ClipboardRuntime(this: any) {
const { minder } = this;
const { receiver } = this;
@ -38,11 +34,10 @@ export default function ClipboardRuntime(this: any) {
return kmencode(Data.getRegisterProtocol('json').encode(_nodes));
}
const beforeCopy = (e: ICliboardEvent) => {
const beforeCopy = (e: ClipboardEvent) => {
if (document.activeElement === receiver.element) {
const clipBoardEvent = e;
const state = this.fsm.state();
switch (state) {
case 'input': {
break;
@ -80,7 +75,7 @@ export default function ClipboardRuntime(this: any) {
}
}
const str = encode(nodes);
clipBoardEvent.clipboardData.setData('text/plain', str);
clipBoardEvent.clipboardData?.setData('text/plain', str);
}
e.preventDefault();
break;
@ -193,7 +188,7 @@ export default function ClipboardRuntime(this: any) {
* @Editor: Naixor
* @Date: 2015.9.24
*/
document.addEventListener('copy', () => beforeCopy);
document.addEventListener('cut', () => beforeCut);
document.addEventListener('paste', () => beforePaste);
document.addEventListener('copy', (e) => beforeCopy(e));
document.addEventListener('cut', (e) => beforeCut(e));
document.addEventListener('paste', (e) => beforePaste(e));
}

View File

@ -51,7 +51,7 @@ class FSM {
}
const oldState = this.currentState;
const notify = [oldState, newState].concat([].slice.call(args, 1));
const notify = [oldState, newState].concat([reason, args[0]]);
let i;
let handler;
@ -67,7 +67,6 @@ class FSM {
this.currentState = newState;
this.debug.log('[{0}] {1} -> {2}', reason, oldState, newState);
// 跳转后
for (i = 0; i < this.handlers.length; i++) {
handler = this.handlers[i];
@ -99,8 +98,9 @@ class FSM {
* * to -
* * reason -
*/
public when(condition: string, handler: Handler | string): void {
if (arguments.length === 1) {
public when(...args: any[]): void {
let [condition, handler] = args;
if (args.length === 1) {
handler = condition;
condition = '* -> *';
}
@ -138,8 +138,8 @@ class FSM {
}
}
function FSMRumtime(this: any) {
function FSMRuntime(this: any) {
this.fsm = new FSM('normal');
}
export default FSMRumtime;
export default FSMRuntime;

View File

@ -1,3 +1,5 @@
import useMinderStore from '@/store/modules/components/minder-editor';
interface Position {
x: number;
y: number;
@ -10,10 +12,11 @@ function HotboxRuntime(this: any) {
const { container } = this;
const { HotBox } = window;
const hotbox = new HotBox(container);
const minderStore = useMinderStore();
hotbox.setParentFSM(fsm);
fsm.when('normal -> hotbox', () => {
function handleHotBoxShow() {
const node = minder.getSelectedNode();
let position: Position | undefined;
if (node) {
@ -22,18 +25,46 @@ function HotboxRuntime(this: any) {
x: box.cx,
y: box.cy,
};
minderStore.dispatchEvent('hotbox', position, node.rc.node);
}
hotbox.active('main', position);
}
fsm.when('normal -> hotbox', () => {
handleHotBoxShow();
});
fsm.when('hotbox -> hotbox', () => {
handleHotBoxShow();
});
function handleShortcut(e: any) {
// 检查是否按下Ctrl键Windows和Linux或Command键Mac
const isCtrlKey = e.ctrlKey || e.metaKey;
// 检查是否按下Enter键
const isEnterKey = e.key === 'Enter';
// 检查是否同时按下Ctrl或Command和Enter键
if (isCtrlKey && isEnterKey) {
// 处理Ctrl+Enter组合键事件
e.preventDefault(); // 阻止默认行为
// 执行进入模块方法
const node = minder.getSelectedNode();
let position: Position | undefined;
if (node) {
const box = node.getRenderBox();
position = {
x: box.cx,
y: box.cy,
};
minderStore.dispatchEvent('enterNode', position, node.rc.node, node.data);
return;
}
}
minder.dispatchKeyEvent(e);
}
fsm.when('normal -> normal', (exit: any, enter: any, reason: any, e: any) => {
if (reason === 'shortcut-handle') {
const handleResult = hotbox.dispatch(e);
if (handleResult) {
e.preventDefault();
} else {
minder.dispatchKeyEvent(e);
}
handleShortcut(e);
}
});

View File

@ -65,7 +65,6 @@ function isIntendToInput(e: KeyboardEvent): boolean {
function JumpingRuntime(this: IJumpingRuntime): void {
const { fsm, minder, receiver, container, hotbox } = this;
const receiverElement = receiver.element;
// normal -> *
receiver.listen('normal', (e: KeyboardEvent) => {
// 为了防止处理进入edit模式而丢失处理的首字母,此时receiver必须为enable
@ -141,6 +140,7 @@ function JumpingRuntime(this: IJumpingRuntime): void {
/// ///////////////////////////////////////////
let downX: number;
let downY: number;
const MOUSE_LB = 1; // 左键
const MOUSE_RB = 2; // 右键
container.addEventListener(
@ -149,10 +149,10 @@ function JumpingRuntime(this: IJumpingRuntime): void {
if (e.button === MOUSE_RB) {
e.preventDefault();
}
if (fsm.state() === 'hotbox') {
hotbox.active(HotBox.STATE_IDLE);
if (fsm.state() === 'hotbox' && e.button === MOUSE_LB) {
fsm.jump('normal', 'blur');
} else if (fsm.state() === 'normal' && e.button === MOUSE_RB) {
} else if (e.button === MOUSE_RB) {
downX = e.clientX;
downY = e.clientY;
}
@ -164,7 +164,6 @@ function JumpingRuntime(this: IJumpingRuntime): void {
'mousewheel',
() => {
if (fsm.state() === 'hotbox') {
hotbox.active(HotBox.STATE_IDLE);
fsm.jump('normal', 'mousemove-blur');
}
},
@ -178,14 +177,14 @@ function JumpingRuntime(this: IJumpingRuntime): void {
container.addEventListener(
'mouseup',
(e) => {
if (fsm.state() !== 'normal') {
if (fsm.state() !== 'normal' && e.button === MOUSE_LB) {
return;
}
if (e.button !== MOUSE_RB || e.clientX !== downX || e.clientY !== downY) {
return;
}
if (!minder.getSelectedNode()) {
return;
return false;
}
fsm.jump('hotbox', 'content-menu');
},

View File

@ -50,6 +50,13 @@ export default function NodeRuntime(this: { minder: any; hotbox: any; editText:
return;
}
markDeleteNode(minder);
} else if (command.indexOf('ArrangeUp') > -1 || command.indexOf('ArrangeDown') > -1) {
if (
!window.minderProps.moveEnable ||
(window.minderProps.moveConfirm && !window.minderProps.moveConfirm())
) {
return;
}
}
minder.execCommand(command);
}

View File

@ -11,33 +11,7 @@ function ReceiverRuntime(this: any) {
// 侦听器,接收到的事件会派发给所有侦听器
// eslint-disable-next-line no-unused-vars
const listeners: ((event: KeyboardEvent) => boolean)[] = [];
const dispatchKeyEvent = (e: KeyboardEvent) => {
(e as any).is = (keyExpression: string) => {
const subs = keyExpression.split('|');
for (let i = 0; i < subs.length; i++) {
if (key.is(this, subs[i])) return true;
}
return false;
};
let listener;
for (let i = 0; i < listeners.length; i++) {
listener = listeners[i];
// 忽略不在侦听状态的侦听器
if ((listener as any).notifyState !== '*' && (listener as any).notifyState !== this.fsm.state()) {
// eslint-disable-next-line no-continue
continue;
}
if (listener.call(null, e)) {
return;
}
}
};
element.onkeydown = element.onkeypress = element.onkeyup = dispatchKeyEvent;
this.container.appendChild(element);
const listeners: (((event: KeyboardEvent) => boolean) | string)[] = [];
interface Receiver {
element: HTMLDivElement;
@ -48,7 +22,7 @@ function ReceiverRuntime(this: any) {
// eslint-disable-next-line no-unused-vars
onblur(handler: (event: FocusEvent) => void): void;
// eslint-disable-next-line no-unused-vars
listen?(state: string, listener: (event: KeyboardEvent) => boolean): void;
listen?(state: string, listener: ((event: KeyboardEvent) => boolean) | string): void;
}
// receiver 对象
@ -76,11 +50,47 @@ function ReceiverRuntime(this: any) {
element.blur();
element.focus();
},
// eslint-disable-next-line no-unused-vars
onblur(handler: (event: FocusEvent) => void) {
element.onblur = handler;
},
};
// 侦听指定状态下的事件,如果不传 state侦听所有状态
receiver.listen = (...args) => {
let [state, listener] = args;
if (args.length <= 1) {
listener = state;
state = '*';
}
(listener as any).notifyState = state;
listeners.push(listener as any);
};
const dispatchKeyEvent = (e: KeyboardEvent) => {
(e as any).is = (keyExpression: string) => {
const subs = keyExpression.split('|');
for (let i = 0; i < subs.length; i++) {
if (key.is(this, subs[i])) return true;
}
return false;
};
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
// 忽略不在侦听状态的侦听器
if ((listener as any).notifyState !== '*' && (listener as any).notifyState !== this.fsm.state()) {
// eslint-disable-next-line no-continue
continue;
}
if (typeof listener === 'function' && listener.call(null, e)) {
return;
}
}
};
element.onkeydown = element.onkeypress = element.onkeyup = dispatchKeyEvent;
this.container.appendChild(element);
receiver.selectAll();
this.minder.on('beforemousedown', receiver.selectAll);
this.minder.on('receiverfocus', receiver.selectAll);
@ -91,22 +101,6 @@ function ReceiverRuntime(this: any) {
window.editor.hotbox.$container.removeChild(window.editor.hotbox.$element);
});
// 侦听指定状态下的事件,如果不传 state侦听所有状态
// eslint-disable-next-line no-unused-vars
receiver.listen = (state: string, listener: ((event: KeyboardEvent) => boolean) | string) => {
if (arguments.length === 1) {
// eslint-disable-next-line no-param-reassign
listener = state;
// eslint-disable-next-line no-param-reassign
state = '*';
}
// eslint-disable-next-line no-param-reassign
if (typeof listener === 'function') {
(listener as any).notifyState = state;
}
listeners.push(listener as any);
};
this.receiver = receiver as any;
}

View File

@ -92,7 +92,7 @@
import { useI18n } from '@/hooks/useI18n';
import { useTableStore } from '@/store';
import { TableOpenDetailMode } from '@/store/modules/ms-table/types';
import { TableOpenDetailMode } from '@/store/modules/components/ms-table/types';
import { MsTableColumn } from './type';
import Draggable from 'vuedraggable';
@ -219,3 +219,4 @@
font-size: 12px;
}
</style>
@/store/modules/components/ms-table/types

View File

@ -4,7 +4,7 @@ import localforage from 'localforage';
import { MsTableColumn, MsTableColumnData } from '@/components/pure/ms-table/type';
import { useAppStore } from '@/store';
import { PageSizeMap, SelectorColumnMap, TableOpenDetailMode } from '@/store/modules/ms-table/types';
import { PageSizeMap, SelectorColumnMap, TableOpenDetailMode } from '@/store/modules/components/ms-table/types';
import { isArraysEqualWithOrder } from '@/utils/equal';
import { SpecialColumnEnum } from '@/enums/tableEnum';

View File

@ -1,7 +1,7 @@
export default {
'common.pleaseSelectMember': 'Please select member',
'common.pleaseSelectMember': 'Please select a member',
'common.add': 'Add',
'common.saveAndContinue': 'Save & Continue',
'common.saveAndContinue': 'Save & continue',
'common.edit': 'Edit',
'common.delete': 'Delete',
'common.save': 'Save',
@ -17,6 +17,23 @@ export default {
'common.close': 'Close',
'common.create': 'Create',
'common.update': 'Update',
'common.all': 'All',
'common.operation': 'Operation',
'common.remove': 'Remove',
'common.revoked': 'Revoked',
'common.createSuccess': 'Create success',
'common.createFailed': 'Create failed',
'common.updateSuccess': 'Update success',
'common.updateFailed': 'Update failed',
'common.deleteConfirm': 'Delete confirm',
'common.deleteSuccess': 'Delete success',
'common.deleteFailed': 'Delete failed',
'common.addSuccess': 'Added successfully',
'common.addFailed': 'Add failed',
'common.editSuccess': 'Edit success',
'common.editFailed': 'Edit failed',
'common.saveSuccess': 'Save success',
'common.saveFailed': 'Save failed',
'common.confirmEnable': 'Confirm enable',
'common.confirmDisable': 'Confirm disable',
'common.confirmClose': 'Confirm close',
@ -25,21 +42,6 @@ export default {
'common.enableFailed': 'Enable failed',
'common.closeSuccess': 'Close success',
'common.closeFailed': 'Close failed',
'common.updateSuccess': 'Update success',
'common.updateFailed': 'Update failed',
'common.all': 'All',
'common.operation': 'Operation',
'common.remove': 'Remove',
'common.revoked': 'Revoked',
'common.deleteConfirm': 'Confirm delete?',
'common.deleteSuccess': 'Delete success',
'common.deleteFailed': 'Delete failed',
'common.addSuccess': 'Add success',
'common.addFailed': 'Add failed',
'common.editSuccess': 'Edit success',
'common.editFailed': 'Edit failed',
'common.saveSuccess': 'Save success',
'common.saveFailed': 'Save failed',
'common.operationSuccess': 'Operation success',
'common.operationFailed': 'Operation failed',
'common.removeSuccess': 'Remove success',
@ -48,18 +50,20 @@ export default {
'common.revokeDelete': 'Revoke delete',
'common.revokeDeleteSuccess': 'Revoke delete success',
'common.unSaveLeaveTitle': 'Leave this page?',
'common.unSaveLeaveContent': 'The system may not save your changes',
'common.unSaveLeaveContent': 'Your changes may not be saved',
'common.leave': 'Leave',
'common.rename': 'Rename',
'common.noData': 'No data',
'common.internal': 'Internal',
'common.custom': 'Custom',
'common.preview': 'Preview',
'common.fullScreen': 'Full Screen',
'common.offFullScreen': 'Exit',
'common.allSelect': 'Select All',
'common.fullScreen': 'Full screen',
'common.offFullScreen': 'Off full screen',
'common.allSelect': 'All select',
'common.setting': 'Setting',
'common.resetDefault': 'Restore default',
'common.pleaseSelect': 'Please Select',
'common.quickAddMember': 'Quick Add Member',
'common.resetDefault': 'Reset default',
'common.tagPlaceholder': 'Add tag and press Enter to end',
'common.batchModify': 'Batch Edit',
'common.pleaseSelect': 'please choose',
'common.quickAddMember': 'Quickly add members',
};

View File

@ -35,10 +35,10 @@ function setI18nLanguage(locale: LocaleType) {
async function changeLocale(locale: LocaleType) {
const globalI18n = i18n.global;
const currentLocale = unref(globalI18n.locale);
Message.loading(currentLocale === 'zh-CN' ? '语言切换中...' : 'Language switching...');
if (currentLocale === locale) {
return locale;
}
Message.loading(currentLocale === 'zh-CN' ? '语言切换中...' : 'Language switching...');
if (loadLocalePool.includes(locale)) {
setI18nLanguage(locale);

View File

@ -4,11 +4,12 @@ import useTableStore from '@/hooks/useTableStore';
import useAppStore from './modules/app';
import useVisitStore from './modules/app/visit';
import useMinderStore from './modules/components/minder-editor';
import useUserStore from './modules/user';
import { debouncePlugin } from './plugins';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
const pinia = createPinia().use(debouncePlugin).use(piniaPluginPersistedstate);
export { useAppStore, useTableStore, useUserStore, useVisitStore };
export { useAppStore, useMinderStore, useTableStore, useUserStore, useVisitStore };
export default pinia;

View File

@ -0,0 +1,36 @@
import { defineStore } from 'pinia';
import { MinderNodePosition, MinderState } from './types';
// 脑图组件的 store
const useMinderStore = defineStore('minder', {
state: (): MinderState => ({
event: {
name: '',
timestamp: 0,
nodePosition: {
x: 0,
y: 0,
},
nodeDom: undefined,
nodeData: undefined,
},
mold: 0,
}),
actions: {
dispatchEvent(name: string, position: MinderNodePosition, nodeDom?: HTMLElement, nodeData?: Record<string, any>) {
this.event = {
name,
timestamp: Date.now(),
nodePosition: position,
nodeDom,
nodeData,
};
},
setMold(val: number) {
this.mold = val;
},
},
});
export default useMinderStore;

View File

@ -0,0 +1,17 @@
export interface MinderNodePosition {
x: number;
y: number;
}
export interface MinderEvent {
name: string;
timestamp: number;
nodePosition: MinderNodePosition;
nodeDom?: HTMLElement;
nodeData?: Record<string, any>;
}
export interface MinderState {
event: MinderEvent;
mold: number;
}

View File

@ -234,6 +234,32 @@ export function findNodeByKey<T>(trees: TreeNode<T>[], targetKey: string, custom
return null; // 如果在整个树形数组中都没有找到匹配的节点,则返回 null
}
/**
* key
*/
export function findNodePathByKey<T>(
tree: TreeNode<T>[],
targetKey: string,
dataKey?: string,
customKey = 'key'
): TreeNode<T> | null {
for (let i = 0; i < tree.length; i++) {
const node = tree[i];
if (dataKey ? node[dataKey]?.[customKey] === targetKey : node[customKey] === targetKey) {
return { ...node, treePath: [dataKey ? node[dataKey] : node] }; // 如果当前节点的 key 与目标 key 匹配,则返回当前节点
}
if (Array.isArray(node.children) && node.children.length > 0) {
const result = findNodePathByKey(node.children, targetKey, dataKey, customKey); // 递归在子节点中查找
if (result) {
result.treePath.unshift(dataKey ? node[dataKey] : node);
return result; // 如果在子节点中找到了匹配的节点,则返回该节点
}
}
}
return null;
}
/**
*
* @param targetMap

View File

@ -33,13 +33,23 @@
</div>
</div>
<FilterPanel v-show="isExpandFilter"></FilterPanel>
<MinderEditor :tags="['模块', '用例', '前置条件', '备注', '步骤', '预期结果']" tag-enable sequence-enable />
<MinderEditor
:import-json="importJson"
:tags="['模块', '用例', '前置条件', '备注', '步骤', '预期结果']"
tag-enable
sequence-enable
@node-click="handleNodeClick"
/>
<MsDrawer v-model:visible="visible" :width="480" :mask="false">
{{ nodeData.text }}
</MsDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MinderEditor from '@/components/pure/minder-editor/minderEditor.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import FilterPanel from '@/components/business/ms-filter-panel/searchForm.vue';
@ -65,6 +75,62 @@
const isExpandFilterHandler = () => {
isExpandFilter.value = !isExpandFilter.value;
};
const visible = ref<boolean>(false);
const nodeData = ref<any>({});
const importJson = ref<any>({});
function handleNodeClick(data: any) {
if (data.resource && data.resource.includes('用例')) {
visible.value = true;
nodeData.value = data;
}
}
onBeforeMount(() => {
importJson.value = {
root: {
data: {
text: '测试用例',
id: 'xxxx',
},
children: [
{
data: {
id: 'sdasdas',
text: '模块 1',
resource: ['模块'],
},
},
{
data: {
id: 'dasdasda',
text: '模块 2',
expandState: 'collapse',
},
children: [
{
data: {
id: 'frihofiuho3f',
text: '用例 1',
resource: ['用例'],
},
},
{
data: {
id: 'df09348f034f',
text: ' 用例 2',
resource: ['用例'],
},
},
],
},
],
},
template: 'default',
};
});
</script>
<style scoped lang="less">

View File

@ -1,11 +1,11 @@
<template>
<a-spin v-if="!projectVersionStatus" :loading="loading" class="h-full w-full">
<a-spin v-if="!projectVersionStatus" :loading="loading" class="h-full w-full min-w-[930px]">
<div class="flex h-full flex-col items-center p-[24px]">
<div class="mt-[200px] text-[16px] font-medium text-[var(--color-text-1)]">
{{ t('project.projectVersion.version') }}
</div>
<div class="mt-[16px] text-[var(--color-text-4)]">{{ t('project.projectVersion.tip') }}</div>
<div class="mt-[24px] flex justify-between gap-[16px]">
<div class="mt-[24px] grid grid-cols-3 gap-[16px]">
<div class="tip-card">
<img src="@/assets/images/project_assign.png" width="78" height="60" class="tip-icon" />
<div>
@ -61,8 +61,11 @@
<template #statusTitle>
<div class="flex items-center">
{{ t('project.projectVersion.status') }}
<a-popover :title="t('project.projectVersion.statusTip')" position="right">
<a-popover position="rt">
<icon-info-circle class="ml-[4px] hover:text-[rgb(var(--primary-5))]" size="16" />
<template #title>
<div class="w-[256px]"> {{ t('project.projectVersion.statusTip') }} </div>
</template>
<template #content>
<div class="mt-[12px] w-[256px] rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[12px]">
<div class="statusTipContent">
@ -172,7 +175,7 @@
:on-before-ok="handleBeforeLatestModalOk"
class="p-[4px]"
title-align="start"
body-class="px-0 py-[8px]"
body-class="p-0"
:ok-text="t('common.confirmClose')"
:ok-button-props="{ status: 'danger' }"
@cancel="handleLatestModalCancel"
@ -515,11 +518,11 @@
},
hideCancel: false,
});
} else {
await toggleVersionStatus(record.id);
Message.success(t('project.projectVersion.open', { name: record.name }));
loadList();
return false;
}
await toggleVersionStatus(record.id);
Message.success(t('project.projectVersion.open', { name: record.name }));
loadList();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);

View File

@ -14,7 +14,7 @@ export default {
'project.projectVersion.openVersionSuccess': 'Activated successfully',
'project.projectVersion.versionName': 'Version name',
'project.projectVersion.status': 'Status',
'project.projectVersion.latest': 'Latest version',
'project.projectVersion.latest': 'Latest',
'project.projectVersion.publishTime': 'Release time',
'project.projectVersion.createTime': 'Creation time',
'project.projectVersion.creator': 'Creator',
@ -34,4 +34,7 @@ export default {
'project.projectVersion.confirmCloseTipContent2': 'You can switch other versions to the latest version',
'project.projectVersion.replaceVersionPlaceholder': 'Please select an alternative version',
'project.projectVersion.latestVersionDeleteTip': 'The latest version cannot be deleted',
'project.projectVersion.statusTip': 'After closing, the version information drop-down box will not be displayed.',
'project.projectVersion.versionInfo': 'Version information example',
'project.projectVersion.versionLatest': 'Latest',
};

View File

@ -275,7 +275,10 @@
const type = ref(''); //
const _module = ref(''); //
const content = ref(''); //
const time = ref<(Date | string | number)[]>([dayjs().subtract(1, 'M').valueOf(), dayjs().valueOf()]); //
const time = ref<(Date | string | number)[]>([
dayjs().subtract(1, 'M').hour(0).minute(0).second(1).valueOf(),
dayjs().valueOf(),
]); //
const selectedTime = ref<Date | string | number | undefined>(''); //
/**