feat: 优化思维导图功能
- 修复新增节点居中逻辑,从当前位置平滑移动到新节点 - 实现节点拖拽保存到数据库功能 - 移除所有'正在加载'弹窗提示 - 优化删除节点时的位置保持 - 添加节点拖拽成功/失败通知
This commit is contained in:
parent
25abc09cb4
commit
3b39f86f83
Binary file not shown.
Binary file not shown.
|
|
@ -1,470 +0,0 @@
|
|||
import "./chunk-FOSKEDPS.js";
|
||||
|
||||
// src/lib/mind-elixir/node_modules/@viselect/vanilla/dist/viselect.mjs
|
||||
var X = class {
|
||||
constructor() {
|
||||
this._listeners = /* @__PURE__ */ new Map(), this.on = this.addEventListener, this.off = this.removeEventListener, this.emit = this.dispatchEvent;
|
||||
}
|
||||
addEventListener(e, t) {
|
||||
const s = this._listeners.get(e) ?? /* @__PURE__ */ new Set();
|
||||
return this._listeners.set(e, s), s.add(t), this;
|
||||
}
|
||||
removeEventListener(e, t) {
|
||||
var s;
|
||||
return (s = this._listeners.get(e)) == null || s.delete(t), this;
|
||||
}
|
||||
dispatchEvent(e, ...t) {
|
||||
let s = true;
|
||||
for (const i of this._listeners.get(e) ?? [])
|
||||
s = i(...t) !== false && s;
|
||||
return s;
|
||||
}
|
||||
unbindAllListeners() {
|
||||
this._listeners.clear();
|
||||
}
|
||||
};
|
||||
var L = (l, e = "px") => typeof l == "number" ? l + e : l;
|
||||
var y = ({ style: l }, e, t) => {
|
||||
if (typeof e == "object")
|
||||
for (const [s, i] of Object.entries(e))
|
||||
i !== void 0 && (l[s] = L(i));
|
||||
else
|
||||
t !== void 0 && (l[e] = L(t));
|
||||
};
|
||||
var M = (l = 0, e = 0, t = 0, s = 0) => {
|
||||
const i = { x: l, y: e, width: t, height: s, top: e, left: l, right: l + t, bottom: e + s };
|
||||
return { ...i, toJSON: () => JSON.stringify(i) };
|
||||
};
|
||||
var Y = (l) => {
|
||||
let e, t = -1, s = false;
|
||||
return {
|
||||
next: (...i) => {
|
||||
e = i, s || (s = true, t = requestAnimationFrame(() => {
|
||||
l(...e), s = false;
|
||||
}));
|
||||
},
|
||||
cancel: () => {
|
||||
cancelAnimationFrame(t), s = false;
|
||||
}
|
||||
};
|
||||
};
|
||||
var k = (l, e, t = "touch") => {
|
||||
switch (t) {
|
||||
case "center": {
|
||||
const s = e.left + e.width / 2, i = e.top + e.height / 2;
|
||||
return s >= l.left && s <= l.right && i >= l.top && i <= l.bottom;
|
||||
}
|
||||
case "cover":
|
||||
return e.left >= l.left && e.top >= l.top && e.right <= l.right && e.bottom <= l.bottom;
|
||||
case "touch":
|
||||
return l.right >= e.left && l.left <= e.right && l.bottom >= e.top && l.top <= e.bottom;
|
||||
}
|
||||
};
|
||||
var H = () => matchMedia("(hover: none), (pointer: coarse)").matches;
|
||||
var N = () => "safari" in window;
|
||||
var A = (l) => Array.isArray(l) ? l : [l];
|
||||
var O = (l) => (e, t, s, i = {}) => {
|
||||
(e instanceof HTMLCollection || e instanceof NodeList) && (e = Array.from(e)), t = A(t), e = A(e);
|
||||
for (const o of e)
|
||||
if (o)
|
||||
for (const n of t)
|
||||
o[l](n, s, { capture: false, ...i });
|
||||
};
|
||||
var S = O("addEventListener");
|
||||
var g = O("removeEventListener");
|
||||
var x = (l) => {
|
||||
var i;
|
||||
const { clientX: e, clientY: t, target: s } = ((i = l.touches) == null ? void 0 : i[0]) ?? l;
|
||||
return { x: e, y: t, target: s };
|
||||
};
|
||||
var E = (l, e = document) => A(l).map(
|
||||
(t) => typeof t == "string" ? Array.from(e.querySelectorAll(t)) : t instanceof Element ? t : null
|
||||
).flat().filter(Boolean);
|
||||
var q = (l, e) => e.some((t) => typeof t == "number" ? l.button === t : typeof t == "object" ? t.button !== l.button ? false : t.modifiers.every((s) => {
|
||||
switch (s) {
|
||||
case "alt":
|
||||
return l.altKey;
|
||||
case "ctrl":
|
||||
return l.ctrlKey || l.metaKey;
|
||||
case "shift":
|
||||
return l.shiftKey;
|
||||
}
|
||||
}) : false);
|
||||
var { abs: b, max: C, min: B, ceil: R } = Math;
|
||||
var D = (l = []) => ({
|
||||
stored: l,
|
||||
selected: [],
|
||||
touched: [],
|
||||
changed: { added: [], removed: [] }
|
||||
});
|
||||
var T = class T2 extends X {
|
||||
constructor(e) {
|
||||
var o, n, r, a, u;
|
||||
super(), this._selection = D(), this._targetBoundaryScrolled = true, this._selectables = [], this._areaLocation = { y1: 0, x2: 0, y2: 0, x1: 0 }, this._areaRect = M(), this._singleClick = true, this._scrollAvailable = true, this._scrollingActive = false, this._scrollSpeed = { x: 0, y: 0 }, this._scrollDelta = { x: 0, y: 0 }, this._lastMousePosition = { x: 0, y: 0 }, this.enable = this._toggleStartEvents, this.disable = this._toggleStartEvents.bind(this, false), this._options = {
|
||||
selectionAreaClass: "selection-area",
|
||||
selectionContainerClass: void 0,
|
||||
selectables: [],
|
||||
document: window.document,
|
||||
startAreas: ["html"],
|
||||
boundaries: ["html"],
|
||||
container: "body",
|
||||
...e,
|
||||
behaviour: {
|
||||
overlap: "invert",
|
||||
intersect: "touch",
|
||||
triggers: [0],
|
||||
...e.behaviour,
|
||||
startThreshold: (o = e.behaviour) != null && o.startThreshold ? typeof e.behaviour.startThreshold == "number" ? e.behaviour.startThreshold : { x: 10, y: 10, ...e.behaviour.startThreshold } : { x: 10, y: 10 },
|
||||
scrolling: {
|
||||
speedDivider: 10,
|
||||
manualSpeed: 750,
|
||||
...(n = e.behaviour) == null ? void 0 : n.scrolling,
|
||||
startScrollMargins: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
...(a = (r = e.behaviour) == null ? void 0 : r.scrolling) == null ? void 0 : a.startScrollMargins
|
||||
}
|
||||
}
|
||||
},
|
||||
features: {
|
||||
range: true,
|
||||
touch: true,
|
||||
deselectOnBlur: false,
|
||||
...e.features,
|
||||
singleTap: {
|
||||
allow: true,
|
||||
intersect: "native",
|
||||
...(u = e.features) == null ? void 0 : u.singleTap
|
||||
}
|
||||
}
|
||||
};
|
||||
for (const _ of Object.getOwnPropertyNames(Object.getPrototypeOf(this)))
|
||||
typeof this[_] == "function" && (this[_] = this[_].bind(this));
|
||||
const { document: t, selectionAreaClass: s, selectionContainerClass: i } = this._options;
|
||||
this._area = t.createElement("div"), this._clippingElement = t.createElement("div"), this._clippingElement.appendChild(this._area), this._area.classList.add(s), i && this._clippingElement.classList.add(i), y(this._area, {
|
||||
willChange: "top, left, bottom, right, width, height",
|
||||
top: 0,
|
||||
left: 0,
|
||||
position: "fixed"
|
||||
}), y(this._clippingElement, {
|
||||
overflow: "hidden",
|
||||
position: "fixed",
|
||||
transform: "translate3d(0, 0, 0)",
|
||||
// https://stackoverflow.com/a/38268846
|
||||
pointerEvents: "none",
|
||||
zIndex: "1"
|
||||
}), this._frame = Y((_) => {
|
||||
this._recalculateSelectionAreaRect(), this._updateElementSelection(), this._emitEvent("move", _), this._redrawSelectionArea();
|
||||
}), this.enable();
|
||||
}
|
||||
_toggleStartEvents(e = true) {
|
||||
const { document: t, features: s } = this._options, i = e ? S : g;
|
||||
i(t, "mousedown", this._onTapStart), s.touch && i(t, "touchstart", this._onTapStart, { passive: false });
|
||||
}
|
||||
_onTapStart(e, t = false) {
|
||||
const { x: s, y: i, target: o } = x(e), { document: n, startAreas: r, boundaries: a, features: u, behaviour: _ } = this._options, c = o.getBoundingClientRect();
|
||||
if (e instanceof MouseEvent && !q(e, _.triggers))
|
||||
return;
|
||||
const p = E(r, n), m = E(a, n);
|
||||
this._targetElement = m.find(
|
||||
(v) => k(v.getBoundingClientRect(), c)
|
||||
);
|
||||
const f = e.composedPath(), d = p.find((v) => f.includes(v));
|
||||
if (this._targetBoundary = m.find((v) => f.includes(v)), !this._targetElement || !d || !this._targetBoundary || !t && this._emitEvent("beforestart", e) === false)
|
||||
return;
|
||||
this._areaLocation = { x1: s, y1: i, x2: 0, y2: 0 };
|
||||
const h = n.scrollingElement ?? n.body;
|
||||
this._scrollDelta = { x: h.scrollLeft, y: h.scrollTop }, this._singleClick = true, this.clearSelection(false, true), S(n, ["touchmove", "mousemove"], this._delayedTapMove, { passive: false }), S(n, ["mouseup", "touchcancel", "touchend"], this._onTapStop), S(n, "scroll", this._onScroll), u.deselectOnBlur && (this._targetBoundaryScrolled = false, S(this._targetBoundary, "scroll", this._onStartAreaScroll));
|
||||
}
|
||||
_onSingleTap(e) {
|
||||
const { singleTap: { intersect: t }, range: s } = this._options.features, i = x(e);
|
||||
let o;
|
||||
if (t === "native")
|
||||
o = i.target;
|
||||
else if (t === "touch") {
|
||||
this.resolveSelectables();
|
||||
const { x: r, y: a } = i;
|
||||
o = this._selectables.find((u) => {
|
||||
const { right: _, left: c, top: p, bottom: m } = u.getBoundingClientRect();
|
||||
return r < _ && r > c && a < m && a > p;
|
||||
});
|
||||
}
|
||||
if (!o)
|
||||
return;
|
||||
for (this.resolveSelectables(); !this._selectables.includes(o); )
|
||||
if (o.parentElement)
|
||||
o = o.parentElement;
|
||||
else {
|
||||
this._targetBoundaryScrolled || this.clearSelection();
|
||||
return;
|
||||
}
|
||||
const { stored: n } = this._selection;
|
||||
if (this._emitEvent("start", e), e.shiftKey && s && this._latestElement) {
|
||||
const r = this._latestElement, [a, u] = r.compareDocumentPosition(o) & 4 ? [o, r] : [r, o], _ = [...this._selectables.filter(
|
||||
(c) => c.compareDocumentPosition(a) & 4 && c.compareDocumentPosition(u) & 2
|
||||
), a, u];
|
||||
this.select(_), this._latestElement = r;
|
||||
} else
|
||||
n.includes(o) && (n.length === 1 || e.ctrlKey || n.every((r) => this._selection.stored.includes(r))) ? this.deselect(o) : (this.select(o), this._latestElement = o);
|
||||
}
|
||||
_delayedTapMove(e) {
|
||||
const { container: t, document: s, behaviour: { startThreshold: i } } = this._options, { x1: o, y1: n } = this._areaLocation, { x: r, y: a } = x(e);
|
||||
if (
|
||||
// Single number for both coordinates
|
||||
typeof i == "number" && b(r + a - (o + n)) >= i || // Different x and y threshold
|
||||
typeof i == "object" && b(r - o) >= i.x || b(a - n) >= i.y
|
||||
) {
|
||||
if (g(s, ["mousemove", "touchmove"], this._delayedTapMove, { passive: false }), this._emitEvent("beforedrag", e) === false) {
|
||||
g(s, ["mouseup", "touchcancel", "touchend"], this._onTapStop);
|
||||
return;
|
||||
}
|
||||
S(s, ["mousemove", "touchmove"], this._onTapMove, { passive: false }), y(this._area, "display", "block"), E(t, s)[0].appendChild(this._clippingElement), this.resolveSelectables(), this._singleClick = false, this._targetRect = this._targetElement.getBoundingClientRect(), this._scrollAvailable = this._targetElement.scrollHeight !== this._targetElement.clientHeight || this._targetElement.scrollWidth !== this._targetElement.clientWidth, this._scrollAvailable && (S(this._targetElement, "wheel", this._wheelScroll, { passive: false }), S(this._options.document, "keydown", this._keyboardScroll, { passive: false }), this._selectables = this._selectables.filter((u) => this._targetElement.contains(u))), this._setupSelectionArea(), this._emitEvent("start", e), this._onTapMove(e);
|
||||
}
|
||||
this._handleMoveEvent(e);
|
||||
}
|
||||
_setupSelectionArea() {
|
||||
const { _clippingElement: e, _targetElement: t, _area: s } = this, i = this._targetRect = t.getBoundingClientRect();
|
||||
this._scrollAvailable ? (y(e, {
|
||||
top: i.top,
|
||||
left: i.left,
|
||||
width: i.width,
|
||||
height: i.height
|
||||
}), y(s, {
|
||||
marginTop: -i.top,
|
||||
marginLeft: -i.left
|
||||
})) : (y(e, {
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: "100%"
|
||||
}), y(s, {
|
||||
marginTop: 0,
|
||||
marginLeft: 0
|
||||
}));
|
||||
}
|
||||
_onTapMove(e) {
|
||||
const { _scrollSpeed: t, _areaLocation: s, _options: i, _frame: o } = this, { speedDivider: n } = i.behaviour.scrolling, r = this._targetElement, { x: a, y: u } = x(e);
|
||||
if (s.x2 = a, s.y2 = u, this._lastMousePosition.x = a, this._lastMousePosition.y = u, this._scrollAvailable && !this._scrollingActive && (t.y || t.x)) {
|
||||
this._scrollingActive = true;
|
||||
const _ = () => {
|
||||
if (!t.x && !t.y) {
|
||||
this._scrollingActive = false;
|
||||
return;
|
||||
}
|
||||
const { scrollTop: c, scrollLeft: p } = r;
|
||||
t.y && (r.scrollTop += R(t.y / n), s.y1 -= r.scrollTop - c), t.x && (r.scrollLeft += R(t.x / n), s.x1 -= r.scrollLeft - p), o.next(e), requestAnimationFrame(_);
|
||||
};
|
||||
requestAnimationFrame(_);
|
||||
} else
|
||||
o.next(e);
|
||||
this._handleMoveEvent(e);
|
||||
}
|
||||
_handleMoveEvent(e) {
|
||||
const { features: t } = this._options;
|
||||
(t.touch && H() || this._scrollAvailable && N()) && e.preventDefault();
|
||||
}
|
||||
_onScroll() {
|
||||
const { _scrollDelta: e, _options: { document: t } } = this, { scrollTop: s, scrollLeft: i } = t.scrollingElement ?? t.body;
|
||||
this._areaLocation.x1 += e.x - i, this._areaLocation.y1 += e.y - s, e.x = i, e.y = s, this._setupSelectionArea(), this._frame.next(null);
|
||||
}
|
||||
_onStartAreaScroll() {
|
||||
this._targetBoundaryScrolled = true, g(this._targetElement, "scroll", this._onStartAreaScroll);
|
||||
}
|
||||
_wheelScroll(e) {
|
||||
const { manualSpeed: t } = this._options.behaviour.scrolling, s = e.deltaY ? e.deltaY > 0 ? 1 : -1 : 0, i = e.deltaX ? e.deltaX > 0 ? 1 : -1 : 0;
|
||||
this._scrollSpeed.y += s * t, this._scrollSpeed.x += i * t, this._onTapMove(e), e.preventDefault();
|
||||
}
|
||||
_keyboardScroll(e) {
|
||||
const { manualSpeed: t } = this._options.behaviour.scrolling, s = e.key === "ArrowLeft" ? -1 : e.key === "ArrowRight" ? 1 : 0, i = e.key === "ArrowUp" ? -1 : e.key === "ArrowDown" ? 1 : 0;
|
||||
this._scrollSpeed.x += Math.sign(s) * t, this._scrollSpeed.y += Math.sign(i) * t, e.preventDefault(), this._onTapMove({
|
||||
clientX: this._lastMousePosition.x,
|
||||
clientY: this._lastMousePosition.y,
|
||||
preventDefault: () => {
|
||||
}
|
||||
});
|
||||
}
|
||||
_recalculateSelectionAreaRect() {
|
||||
const { _scrollSpeed: e, _areaLocation: t, _targetElement: s, _options: i } = this, { scrollTop: o, scrollHeight: n, clientHeight: r, scrollLeft: a, scrollWidth: u, clientWidth: _ } = s, c = this._targetRect, { x1: p, y1: m } = t;
|
||||
let { x2: f, y2: d } = t;
|
||||
const { behaviour: { scrolling: { startScrollMargins: h } } } = i;
|
||||
f < c.left + h.x ? (e.x = a ? -b(c.left - f + h.x) : 0, f = f < c.left ? c.left : f) : f > c.right - h.x ? (e.x = u - a - _ ? b(c.left + c.width - f - h.x) : 0, f = f > c.right ? c.right : f) : e.x = 0, d < c.top + h.y ? (e.y = o ? -b(c.top - d + h.y) : 0, d = d < c.top ? c.top : d) : d > c.bottom - h.y ? (e.y = n - o - r ? b(c.top + c.height - d - h.y) : 0, d = d > c.bottom ? c.bottom : d) : e.y = 0;
|
||||
const v = B(p, f), w = B(m, d), j = C(p, f), K = C(m, d);
|
||||
this._areaRect = M(v, w, j - v, K - w);
|
||||
}
|
||||
_redrawSelectionArea() {
|
||||
const { x: e, y: t, width: s, height: i } = this._areaRect, { style: o } = this._area;
|
||||
o.left = `${e}px`, o.top = `${t}px`, o.width = `${s}px`, o.height = `${i}px`;
|
||||
}
|
||||
_onTapStop(e, t) {
|
||||
var n;
|
||||
const { document: s, features: i } = this._options, { _singleClick: o } = this;
|
||||
g(this._targetElement, "scroll", this._onStartAreaScroll), g(s, ["mousemove", "touchmove"], this._delayedTapMove), g(s, ["touchmove", "mousemove"], this._onTapMove), g(s, ["mouseup", "touchcancel", "touchend"], this._onTapStop), g(s, "scroll", this._onScroll), this._keepSelection(), e && o && i.singleTap.allow ? this._onSingleTap(e) : !o && !t && (this._updateElementSelection(), this._emitEvent("stop", e)), this._scrollSpeed.x = 0, this._scrollSpeed.y = 0, g(this._targetElement, "wheel", this._wheelScroll, { passive: true }), g(this._options.document, "keydown", this._keyboardScroll, { passive: true }), this._clippingElement.remove(), (n = this._frame) == null || n.cancel(), y(this._area, "display", "none");
|
||||
}
|
||||
_updateElementSelection() {
|
||||
const { _selectables: e, _options: t, _selection: s, _areaRect: i } = this, { stored: o, selected: n, touched: r } = s, { intersect: a, overlap: u } = t.behaviour, _ = u === "invert", c = [], p = [], m = [];
|
||||
for (let d = 0; d < e.length; d++) {
|
||||
const h = e[d];
|
||||
if (k(i, h.getBoundingClientRect(), a)) {
|
||||
if (n.includes(h))
|
||||
o.includes(h) && !r.includes(h) && r.push(h);
|
||||
else if (_ && o.includes(h)) {
|
||||
m.push(h);
|
||||
continue;
|
||||
} else
|
||||
p.push(h);
|
||||
c.push(h);
|
||||
}
|
||||
}
|
||||
_ && p.push(...o.filter((d) => !n.includes(d)));
|
||||
const f = u === "keep";
|
||||
for (let d = 0; d < n.length; d++) {
|
||||
const h = n[d];
|
||||
!c.includes(h) && !// Check if the user wants to keep previously selected elements, e.g.,
|
||||
// not make them part of the current selection as soon as they're touched.
|
||||
(f && o.includes(h)) && m.push(h);
|
||||
}
|
||||
s.selected = c, s.changed = { added: p, removed: m }, this._latestElement = void 0;
|
||||
}
|
||||
_emitEvent(e, t) {
|
||||
return this.emit(e, {
|
||||
event: t,
|
||||
store: this._selection,
|
||||
selection: this
|
||||
});
|
||||
}
|
||||
_keepSelection() {
|
||||
const { _options: e, _selection: t } = this, { selected: s, changed: i, touched: o, stored: n } = t, r = s.filter((a) => !n.includes(a));
|
||||
switch (e.behaviour.overlap) {
|
||||
case "drop": {
|
||||
t.stored = [
|
||||
...r,
|
||||
...n.filter((a) => !o.includes(a))
|
||||
// Elements not touched
|
||||
];
|
||||
break;
|
||||
}
|
||||
case "invert": {
|
||||
t.stored = [
|
||||
...r,
|
||||
...n.filter((a) => !i.removed.includes(a))
|
||||
// Elements not removed from selection
|
||||
];
|
||||
break;
|
||||
}
|
||||
case "keep": {
|
||||
t.stored = [
|
||||
...n,
|
||||
...s.filter((a) => !n.includes(a))
|
||||
// Newly added
|
||||
];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Manually triggers the start of a selection
|
||||
* @param evt A MouseEvent / TouchEvent-like object
|
||||
* @param silent If beforestart should be fired
|
||||
*/
|
||||
trigger(e, t = true) {
|
||||
this._onTapStart(e, t);
|
||||
}
|
||||
/**
|
||||
* Can be used if during a selection elements have been added
|
||||
* Will update everything that can be selected
|
||||
*/
|
||||
resolveSelectables() {
|
||||
this._selectables = E(this._options.selectables, this._options.document);
|
||||
}
|
||||
/**
|
||||
* Same as deselecting, but for all elements currently selected
|
||||
* @param includeStored If the store should also get cleared
|
||||
* @param quiet If move / stop events should be fired
|
||||
*/
|
||||
clearSelection(e = true, t = false) {
|
||||
const { selected: s, stored: i, changed: o } = this._selection;
|
||||
o.added = [], o.removed.push(
|
||||
...s,
|
||||
...e ? i : []
|
||||
), t || (this._emitEvent("move", null), this._emitEvent("stop", null)), this._selection = D(e ? [] : i);
|
||||
}
|
||||
/**
|
||||
* @returns {Array} Selected elements
|
||||
*/
|
||||
getSelection() {
|
||||
return this._selection.stored;
|
||||
}
|
||||
/**
|
||||
* @returns {HTMLElement} The selection area element
|
||||
*/
|
||||
getSelectionArea() {
|
||||
return this._area;
|
||||
}
|
||||
/**
|
||||
* @returns {Element[]} Available selectable elements for current selection
|
||||
*/
|
||||
getSelectables() {
|
||||
return this._selectables;
|
||||
}
|
||||
/**
|
||||
* Set the location of the selection area
|
||||
* @param location A partial AreaLocation object
|
||||
*/
|
||||
setAreaLocation(e) {
|
||||
Object.assign(this._areaLocation, e), this._redrawSelectionArea();
|
||||
}
|
||||
/**
|
||||
* @returns {AreaLocation} The current location of the selection area
|
||||
*/
|
||||
getAreaLocation() {
|
||||
return this._areaLocation;
|
||||
}
|
||||
/**
|
||||
* Cancel the current selection process, pass true to fire a stop event after cancel
|
||||
* @param keepEvent If a stop event should be fired
|
||||
*/
|
||||
cancel(e = false) {
|
||||
this._onTapStop(null, !e);
|
||||
}
|
||||
/**
|
||||
* Unbinds all events and removes the area-element.
|
||||
*/
|
||||
destroy() {
|
||||
this.cancel(), this.disable(), this._clippingElement.remove(), super.unbindAllListeners();
|
||||
}
|
||||
/**
|
||||
* Adds elements to the selection
|
||||
* @param query CSS Query, can be an array of queries
|
||||
* @param quiet If this should not trigger the move event
|
||||
*/
|
||||
select(e, t = false) {
|
||||
const { changed: s, selected: i, stored: o } = this._selection, n = E(e, this._options.document).filter(
|
||||
(r) => !i.includes(r) && !o.includes(r)
|
||||
);
|
||||
return o.push(...n), i.push(...n), s.added.push(...n), s.removed = [], this._latestElement = void 0, t || (this._emitEvent("move", null), this._emitEvent("stop", null)), n;
|
||||
}
|
||||
/**
|
||||
* Removes a particular element from the selection
|
||||
* @param query CSS Query, can be an array of queries
|
||||
* @param quiet If this should not trigger the move event
|
||||
*/
|
||||
deselect(e, t = false) {
|
||||
const { selected: s, stored: i, changed: o } = this._selection, n = E(e, this._options.document).filter(
|
||||
(r) => s.includes(r) || i.includes(r)
|
||||
);
|
||||
this._selection.stored = i.filter((r) => !n.includes(r)), this._selection.selected = s.filter((r) => !n.includes(r)), this._selection.changed.added = [], this._selection.changed.removed.push(
|
||||
...n.filter((r) => !o.removed.includes(r))
|
||||
), this._latestElement = void 0, t || (this._emitEvent("move", null), this._emitEvent("stop", null));
|
||||
}
|
||||
};
|
||||
T.version = "3.9.0";
|
||||
var P = T;
|
||||
export {
|
||||
P as default
|
||||
};
|
||||
/*! Bundled license information:
|
||||
|
||||
@viselect/vanilla/dist/viselect.mjs:
|
||||
(*! @viselect/vanilla v3.9.0 MIT | https://github.com/Simonwep/selection/tree/master/packages/vanilla *)
|
||||
*/
|
||||
//# sourceMappingURL=@viselect_vanilla.js.map
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,83 +1,77 @@
|
|||
{
|
||||
"hash": "76772e52",
|
||||
"browserHash": "a4ef7769",
|
||||
"hash": "94dd0fae",
|
||||
"browserHash": "b8442f90",
|
||||
"optimized": {
|
||||
"axios": {
|
||||
"src": "../../axios/index.js",
|
||||
"file": "axios.js",
|
||||
"fileHash": "923e7809",
|
||||
"needsInterop": false
|
||||
},
|
||||
"mammoth": {
|
||||
"src": "../../mammoth/lib/index.js",
|
||||
"file": "mammoth.js",
|
||||
"fileHash": "2d23e669",
|
||||
"needsInterop": true
|
||||
},
|
||||
"marked": {
|
||||
"src": "../../marked/lib/marked.esm.js",
|
||||
"file": "marked.js",
|
||||
"fileHash": "e6802b70",
|
||||
"needsInterop": false
|
||||
},
|
||||
"pdfjs-dist": {
|
||||
"src": "../../pdfjs-dist/build/pdf.mjs",
|
||||
"file": "pdfjs-dist.js",
|
||||
"fileHash": "b0d6a144",
|
||||
"needsInterop": false
|
||||
},
|
||||
"prismjs": {
|
||||
"src": "../../prismjs/prism.js",
|
||||
"file": "prismjs.js",
|
||||
"fileHash": "0c7d8fc7",
|
||||
"needsInterop": true
|
||||
},
|
||||
"prismjs/components/prism-css": {
|
||||
"src": "../../prismjs/components/prism-css.js",
|
||||
"file": "prismjs_components_prism-css.js",
|
||||
"fileHash": "b5dc8638",
|
||||
"needsInterop": true
|
||||
},
|
||||
"prismjs/components/prism-javascript": {
|
||||
"src": "../../prismjs/components/prism-javascript.js",
|
||||
"file": "prismjs_components_prism-javascript.js",
|
||||
"fileHash": "a3fb501c",
|
||||
"needsInterop": true
|
||||
},
|
||||
"prismjs/components/prism-json": {
|
||||
"src": "../../prismjs/components/prism-json.js",
|
||||
"file": "prismjs_components_prism-json.js",
|
||||
"fileHash": "2d44f86b",
|
||||
"needsInterop": true
|
||||
},
|
||||
"prismjs/components/prism-python": {
|
||||
"src": "../../prismjs/components/prism-python.js",
|
||||
"file": "prismjs_components_prism-python.js",
|
||||
"fileHash": "ca5259af",
|
||||
"needsInterop": true
|
||||
},
|
||||
"prismjs/components/prism-sql": {
|
||||
"src": "../../prismjs/components/prism-sql.js",
|
||||
"file": "prismjs_components_prism-sql.js",
|
||||
"fileHash": "4a89443c",
|
||||
"needsInterop": true
|
||||
},
|
||||
"vue": {
|
||||
"src": "../../vue/dist/vue.runtime.esm-bundler.js",
|
||||
"file": "vue.js",
|
||||
"fileHash": "b4ce9a46",
|
||||
"fileHash": "d8567397",
|
||||
"needsInterop": false
|
||||
},
|
||||
"katex": {
|
||||
"src": "../../katex/dist/katex.mjs",
|
||||
"file": "katex.js",
|
||||
"fileHash": "e7ed5c7a",
|
||||
"fileHash": "7dd124eb",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@viselect/vanilla": {
|
||||
"src": "../../../src/lib/mind-elixir/node_modules/@viselect/vanilla/dist/viselect.mjs",
|
||||
"file": "@viselect_vanilla.js",
|
||||
"fileHash": "b39569df",
|
||||
"mammoth": {
|
||||
"src": "../../mammoth/lib/index.js",
|
||||
"file": "mammoth.js",
|
||||
"fileHash": "9aaed3ae",
|
||||
"needsInterop": true
|
||||
},
|
||||
"marked": {
|
||||
"src": "../../marked/lib/marked.esm.js",
|
||||
"file": "marked.js",
|
||||
"fileHash": "d5cd504b",
|
||||
"needsInterop": false
|
||||
},
|
||||
"pdfjs-dist": {
|
||||
"src": "../../pdfjs-dist/build/pdf.mjs",
|
||||
"file": "pdfjs-dist.js",
|
||||
"fileHash": "4804df17",
|
||||
"needsInterop": false
|
||||
},
|
||||
"prismjs": {
|
||||
"src": "../../prismjs/prism.js",
|
||||
"file": "prismjs.js",
|
||||
"fileHash": "13932d2e",
|
||||
"needsInterop": true
|
||||
},
|
||||
"prismjs/components/prism-css": {
|
||||
"src": "../../prismjs/components/prism-css.js",
|
||||
"file": "prismjs_components_prism-css.js",
|
||||
"fileHash": "f24b2e04",
|
||||
"needsInterop": true
|
||||
},
|
||||
"prismjs/components/prism-javascript": {
|
||||
"src": "../../prismjs/components/prism-javascript.js",
|
||||
"file": "prismjs_components_prism-javascript.js",
|
||||
"fileHash": "b44ed50e",
|
||||
"needsInterop": true
|
||||
},
|
||||
"prismjs/components/prism-json": {
|
||||
"src": "../../prismjs/components/prism-json.js",
|
||||
"file": "prismjs_components_prism-json.js",
|
||||
"fileHash": "0d3b4905",
|
||||
"needsInterop": true
|
||||
},
|
||||
"prismjs/components/prism-python": {
|
||||
"src": "../../prismjs/components/prism-python.js",
|
||||
"file": "prismjs_components_prism-python.js",
|
||||
"fileHash": "f1831121",
|
||||
"needsInterop": true
|
||||
},
|
||||
"prismjs/components/prism-sql": {
|
||||
"src": "../../prismjs/components/prism-sql.js",
|
||||
"file": "prismjs_components_prism-sql.js",
|
||||
"fileHash": "e04f8e66",
|
||||
"needsInterop": true
|
||||
},
|
||||
"vue": {
|
||||
"src": "../../vue/dist/vue.runtime.esm-bundler.js",
|
||||
"file": "vue.js",
|
||||
"fileHash": "cba89b1c",
|
||||
"needsInterop": false
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,13 +15,13 @@ export {};
|
|||
type __VLS_PickNotAny<A, B> = __VLS_IsAny<A> extends true ? B : A;
|
||||
type __VLS_SpreadMerge<A, B> = Omit<A, keyof B> & B;
|
||||
type __VLS_WithComponent<N0 extends string, LocalComponents, Self, N1 extends string, N2 extends string, N3 extends string> =
|
||||
N1 extends keyof LocalComponents ? N1 extends N0 ? Pick<LocalComponents, N0 extends keyof LocalComponents ? N0 : never> : { [K in N0]: LocalComponents[N1] } :
|
||||
N2 extends keyof LocalComponents ? N2 extends N0 ? Pick<LocalComponents, N0 extends keyof LocalComponents ? N0 : never> : { [K in N0]: LocalComponents[N2] } :
|
||||
N3 extends keyof LocalComponents ? N3 extends N0 ? Pick<LocalComponents, N0 extends keyof LocalComponents ? N0 : never> : { [K in N0]: LocalComponents[N3] } :
|
||||
N1 extends keyof LocalComponents ? { [K in N0]: LocalComponents[N1] } :
|
||||
N2 extends keyof LocalComponents ? { [K in N0]: LocalComponents[N2] } :
|
||||
N3 extends keyof LocalComponents ? { [K in N0]: LocalComponents[N3] } :
|
||||
Self extends object ? { [K in N0]: Self } :
|
||||
N1 extends keyof __VLS_GlobalComponents ? N1 extends N0 ? Pick<__VLS_GlobalComponents, N0 extends keyof __VLS_GlobalComponents ? N0 : never> : { [K in N0]: __VLS_GlobalComponents[N1] } :
|
||||
N2 extends keyof __VLS_GlobalComponents ? N2 extends N0 ? Pick<__VLS_GlobalComponents, N0 extends keyof __VLS_GlobalComponents ? N0 : never> : { [K in N0]: __VLS_GlobalComponents[N2] } :
|
||||
N3 extends keyof __VLS_GlobalComponents ? N3 extends N0 ? Pick<__VLS_GlobalComponents, N0 extends keyof __VLS_GlobalComponents ? N0 : never> : { [K in N0]: __VLS_GlobalComponents[N3] } :
|
||||
N1 extends keyof __VLS_GlobalComponents ? { [K in N0]: __VLS_GlobalComponents[N1] } :
|
||||
N2 extends keyof __VLS_GlobalComponents ? { [K in N0]: __VLS_GlobalComponents[N2] } :
|
||||
N3 extends keyof __VLS_GlobalComponents ? { [K in N0]: __VLS_GlobalComponents[N3] } :
|
||||
{};
|
||||
type __VLS_FunctionalComponentCtx<T, K> = __VLS_PickNotAny<'__ctx' extends keyof __VLS_PickNotAny<K, {}>
|
||||
? K extends { __ctx?: infer Ctx } ? NonNullable<Ctx> : never : any
|
||||
|
|
@ -83,6 +83,10 @@ export {};
|
|||
}
|
||||
>
|
||||
>;
|
||||
type __VLS_EmitsToProps<T> = __VLS_PrettifyGlobal<{
|
||||
[K in string & keyof T as `on${Capitalize<K>}`]?:
|
||||
(...args: T[K] extends (...args: infer P) => any ? P : T[K] extends null ? any[] : never) => any;
|
||||
}>;
|
||||
type __VLS_ResolveEmits<
|
||||
Comp,
|
||||
Emits,
|
||||
|
|
@ -90,10 +94,16 @@ export {};
|
|||
NormalizedEmits = __VLS_NormalizeEmits<Emits> extends infer E ? string extends keyof E ? {} : E : never,
|
||||
> = __VLS_SpreadMerge<NormalizedEmits, TypeEmits>;
|
||||
type __VLS_ResolveDirectives<T> = {
|
||||
[K in Exclude<keyof T, keyof __VLS_GlobalDirectives> & string as `v${Capitalize<K>}`]: T[K];
|
||||
[K in keyof T & string as `v${Capitalize<K>}`]: T[K];
|
||||
};
|
||||
type __VLS_PrettifyGlobal<T> = { [K in keyof T as K]: T[K]; } & {};
|
||||
type __VLS_WithDefaultsGlobal<P, D> = {
|
||||
[K in keyof P as K extends keyof D ? K : never]-?: P[K];
|
||||
} & {
|
||||
[K in keyof P as K extends keyof D ? never : K]: P[K];
|
||||
};
|
||||
type __VLS_UseTemplateRef<T> = Readonly<import('vue').ShallowRef<T | null>>;
|
||||
type __VLS_ProxyRefs<T> = import('vue').ShallowUnwrapRef<T>;
|
||||
|
||||
function __VLS_getVForSourceType<T extends number | string | any[] | Iterable<any>>(source: T): [
|
||||
item: T extends number ? number
|
||||
|
|
@ -115,7 +125,6 @@ export {};
|
|||
: T extends (...args: any) => any
|
||||
? T
|
||||
: (arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown) => void;
|
||||
function __VLS_makeOptional<T>(t: T): { [K in keyof T]?: T[K] };
|
||||
function __VLS_asFunctionalComponent<T, K = T extends new (...args: any) => any ? InstanceType<T> : unknown>(t: T, instance?: K):
|
||||
T extends new (...args: any) => any ? __VLS_FunctionalComponent<K>
|
||||
: T extends () => any ? (props: {}, ctx?: any) => ReturnType<T>
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div id="app">« <!-- 测试模式切换按钮 -->
|
||||
<div class="test-mode-toggle">
|
||||
<div class="test-mode-toggle" style="display: none;">
|
||||
<button @click="toggleTestMode" class="test-btn">
|
||||
{{ isTestMode ? '切换到思维导图' : '测试Markdown渲染' }}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -303,9 +303,9 @@ const generateMarkdownFromFile = async () => {
|
|||
await convertToJSON();
|
||||
|
||||
// 添加到历史记录
|
||||
addToHistory('AI生成: ' + uploadedFile.value.name, markdownContent.value);
|
||||
addToHistory(uploadedFile.value.name, markdownContent.value);
|
||||
|
||||
showNotification('AI生成Markdown成功!正在自动保存...', 'success');
|
||||
// showNotification('AI生成Markdown成功!正在自动保存...', 'success');
|
||||
|
||||
// 延迟一下让用户看到成功消息,然后自动保存
|
||||
setTimeout(async () => {
|
||||
|
|
@ -437,14 +437,14 @@ const generateMarkdown = async () => {
|
|||
await convertToJSON();
|
||||
|
||||
// 添加到历史记录
|
||||
addToHistory('AI生成: ' + aiPrompt.value.substring(0, 30) + '...', markdownContent.value);
|
||||
addToHistory(aiPrompt.value.substring(0, 30) + '...', markdownContent.value);
|
||||
|
||||
// 确保AI提示词不被清空
|
||||
if (aiPrompt.value !== savedAiPrompt.value) {
|
||||
aiPrompt.value = savedAiPrompt.value;
|
||||
}
|
||||
|
||||
showNotification('AI生成Markdown成功!正在自动保存...', 'success');
|
||||
// showNotification('AI生成Markdown成功!正在自动保存...', 'success');
|
||||
|
||||
// 延迟一下让用户看到成功消息,然后自动保存
|
||||
setTimeout(async () => {
|
||||
|
|
@ -627,7 +627,7 @@ Level 4 标题用 #####
|
|||
try {
|
||||
|
||||
// 显示进度提示
|
||||
showNotification('AI正在分析文档,请耐心等待(最多2分钟)...', 'info');
|
||||
// showNotification('AI正在分析文档,请耐心等待(最多2分钟)...', 'info');
|
||||
|
||||
// 添加超时处理 - 增加超时时间,处理复杂文档
|
||||
const controller = new AbortController();
|
||||
|
|
@ -685,7 +685,7 @@ Level 4 标题用 #####
|
|||
|
||||
if (isTruncated && retryCount < 2) {
|
||||
console.warn(`⚠️ 检测到内容可能被截断,进行第${retryCount + 1}次重试`);
|
||||
showNotification(`检测到内容可能被截断,正在重试...(${retryCount + 1}/2)`, 'warning');
|
||||
// showNotification(`检测到内容可能被截断,正在重试...(${retryCount + 1}/2)`, 'warning');
|
||||
|
||||
// 等待1秒后重试
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
|
@ -1099,7 +1099,7 @@ const importToMindmap = () => {
|
|||
window.dispatchEvent(event);
|
||||
|
||||
// 显示成功通知而不是alert
|
||||
showNotification('已触发导入事件,正在处理...', 'success');
|
||||
// showNotification('已触发导入事件,正在处理...', 'success');
|
||||
} catch (error) {
|
||||
console.error('JSON解析失败:', error);
|
||||
showNotification('JSON格式错误,请检查数据', 'error');
|
||||
|
|
@ -1116,8 +1116,8 @@ const previewMindmap = async () => {
|
|||
return;
|
||||
}
|
||||
|
||||
isProcessing.value = true;
|
||||
processingMessage.value = '正在保存思维导图...';
|
||||
// isProcessing.value = true;
|
||||
// processingMessage.value = '正在保存思维导图...';
|
||||
|
||||
try {
|
||||
const mindmapData = JSON.parse(convertedJSON.value);
|
||||
|
|
@ -1141,8 +1141,8 @@ const previewMindmap = async () => {
|
|||
|
||||
// 延迟显示成功消息
|
||||
setTimeout(() => {
|
||||
isProcessing.value = false;
|
||||
processingMessage.value = '';
|
||||
// isProcessing.value = false;
|
||||
// processingMessage.value = '';
|
||||
showNotification('思维导图已保存成功!', 'success');
|
||||
|
||||
// 注释掉自动清空功能,保留数据用于调试
|
||||
|
|
@ -1159,8 +1159,8 @@ const previewMindmap = async () => {
|
|||
}, 2000);
|
||||
|
||||
} catch (error) {
|
||||
isProcessing.value = false;
|
||||
processingMessage.value = '';
|
||||
// isProcessing.value = false;
|
||||
// processingMessage.value = '';
|
||||
console.error('JSON解析失败:', error);
|
||||
showNotification('JSON格式错误,请检查数据', 'error');
|
||||
}
|
||||
|
|
@ -1422,7 +1422,7 @@ const loadHistoryItem = async (item) => {
|
|||
}));
|
||||
|
||||
// 显示加载通知
|
||||
showNotification(`正在加载: ${item.title}`, 'info');
|
||||
// showNotification(`正在加载: ${item.title}`, 'info');
|
||||
} else {
|
||||
// 如果没有ID,使用传统方式加载内容
|
||||
markdownContent.value = item.content;
|
||||
|
|
@ -1440,7 +1440,7 @@ const loadHistoryItem = async (item) => {
|
|||
}));
|
||||
|
||||
// 显示加载通知
|
||||
showNotification(`正在加载: ${item.title}`, 'info');
|
||||
// showNotification(`正在加载: ${item.title}`, 'info');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,33 +1,5 @@
|
|||
<template>
|
||||
<div class="mindmap-container">
|
||||
<!-- 缩放控制按钮 -->
|
||||
<div class="zoom-controls" :class="{ 'welcome-mode': showWelcome }">
|
||||
<button @click="zoomIn" class="zoom-btn" title="放大">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<path d="m21 21-4.35-4.35"></path>
|
||||
<path d="M11 8v6"></path>
|
||||
<path d="M8 11h6"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button @click="zoomOut" class="zoom-btn" title="缩小">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<path d="m21 21-4.35-4.35"></path>
|
||||
<path d="M8 11h6"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button @click="resetZoom" class="zoom-btn" title="重置缩放">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"></path>
|
||||
<path d="M21 3v5h-5"></path>
|
||||
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"></path>
|
||||
<path d="M3 21v-5h5"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="zoom-level">{{ Math.round(zoomLevel * 100) }}%</span>
|
||||
</div>
|
||||
|
||||
<!-- 欢迎页面 -->
|
||||
<div v-if="showWelcome" class="welcome-page">
|
||||
<div class="welcome-content" :class="{ 'ai-sidebar-collapsed': isAISidebarCollapsed }">
|
||||
|
|
@ -105,7 +77,7 @@
|
|||
|
||||
<!-- 保存和刷新按钮 -->
|
||||
<div v-if="!showWelcome" class="save-controls">
|
||||
<button @click="saveMindMap" class="save-btn" title="保存思维导图">
|
||||
<button @click="saveMindMap" class="save-btn" title="保存思维导图" style="display: none;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
|
||||
<polyline points="17,21 17,13 7,13 7,21"></polyline>
|
||||
|
|
@ -114,7 +86,7 @@
|
|||
<span>保存</span>
|
||||
</button>
|
||||
|
||||
<button @click="refreshMindMap" class="refresh-btn" title="刷新思维导图">
|
||||
<button @click="refreshMindMap" class="refresh-btn" title="刷新思维导图" style="display: none;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M1 4v6h6"></path>
|
||||
<path d="M23 20v-6h-6"></path>
|
||||
|
|
@ -282,10 +254,13 @@ const saveCurrentPosition = () => {
|
|||
if (!mindElixir.value || !mindmapEl.value) return null;
|
||||
|
||||
try {
|
||||
const canvas = mindmapEl.value.querySelector('canvas');
|
||||
if (canvas) {
|
||||
const transform = canvas.style.transform;
|
||||
return { transform };
|
||||
// MindElixir 使用 .map-canvas 元素存储 transform 样式
|
||||
const mapCanvas = mindmapEl.value.querySelector('.map-canvas');
|
||||
if (mapCanvas) {
|
||||
const transform = mapCanvas.style.transform;
|
||||
const scaleVal = mindElixir.value.scaleVal || 1;
|
||||
console.log('📍 保存位置:', { transform, scaleVal });
|
||||
return { transform, scaleVal };
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('保存位置失败:', error);
|
||||
|
|
@ -295,12 +270,24 @@ const saveCurrentPosition = () => {
|
|||
|
||||
// 恢复思维导图位置和缩放
|
||||
const restorePosition = (position) => {
|
||||
if (!position || !mindmapEl.value) return;
|
||||
if (!position || !mindmapEl.value || !mindElixir.value) return;
|
||||
|
||||
try {
|
||||
const canvas = mindmapEl.value.querySelector('canvas');
|
||||
if (canvas && position.transform) {
|
||||
canvas.style.transform = position.transform;
|
||||
// MindElixir 使用 .map-canvas 元素存储 transform 样式
|
||||
const mapCanvas = mindmapEl.value.querySelector('.map-canvas');
|
||||
if (mapCanvas && position.transform) {
|
||||
// 强制设置 transform,使用 !important 确保优先级
|
||||
mapCanvas.style.setProperty('transform', position.transform, 'important');
|
||||
|
||||
// 恢复缩放级别
|
||||
if (position.scaleVal && mindElixir.value.scaleVal !== position.scaleVal) {
|
||||
mindElixir.value.scaleVal = position.scaleVal;
|
||||
}
|
||||
|
||||
// 强制设置 transform-origin
|
||||
mapCanvas.style.setProperty('transform-origin', 'center center', 'important');
|
||||
|
||||
console.log('📍 恢复位置:', { transform: position.transform, scaleVal: position.scaleVal });
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('恢复位置失败:', error);
|
||||
|
|
@ -308,10 +295,16 @@ const restorePosition = (position) => {
|
|||
};
|
||||
|
||||
// 加载思维导图数据 - 支持位置保持
|
||||
const loadMindmapData = async (data, keepPosition = false) => {
|
||||
const loadMindmapData = async (data, keepPosition = false, shouldCenterRoot = true) => {
|
||||
try {
|
||||
// 保存当前位置(如果需要保持位置)
|
||||
// 只有在明确需要保持位置时才保存当前位置
|
||||
// 对于添加节点的情况(keepPosition=false, shouldCenterRoot=false),不保存位置,让视图自然居中到新节点
|
||||
const currentPosition = keepPosition ? saveCurrentPosition() : null;
|
||||
if (currentPosition) {
|
||||
console.log('📍 保存当前位置用于保持原位:', currentPosition);
|
||||
} else if (!keepPosition && !shouldCenterRoot) {
|
||||
console.log('📍 添加节点模式:不保存位置,等待居中到新节点');
|
||||
}
|
||||
|
||||
// 设置当前思维导图ID
|
||||
console.log("🔍 loadMindmapData 被调用,数据:", data);
|
||||
|
|
@ -365,10 +358,10 @@ const loadMindmapData = async (data, keepPosition = false) => {
|
|||
el: mindmapEl.value,
|
||||
direction: MindElixir.RIGHT,
|
||||
draggable: true,
|
||||
contextMenu: true,
|
||||
contextMenu: false,
|
||||
toolBar: true,
|
||||
nodeMenu: false,
|
||||
keypress: true,
|
||||
keypress: false, // 禁用键盘快捷键,防止不入库的添加节点操作
|
||||
autoCenter: false,
|
||||
infinite: true,
|
||||
maxScale: 5,
|
||||
|
|
@ -393,6 +386,22 @@ const loadMindmapData = async (data, keepPosition = false) => {
|
|||
const result = mindElixir.value.init(data);
|
||||
console.log('✅ Mind Elixir实例创建成功,初始化结果:', result);
|
||||
|
||||
// 如果有保存的位置,立即恢复位置(因为 init 会自动居中)
|
||||
if (currentPosition) {
|
||||
// 立即恢复位置,不延迟
|
||||
restorePosition(currentPosition);
|
||||
console.log('📍 初始化后立即恢复位置');
|
||||
|
||||
// 再次确保位置恢复(防止被其他操作覆盖)
|
||||
setTimeout(() => {
|
||||
restorePosition(currentPosition);
|
||||
console.log('📍 二次确认位置恢复');
|
||||
}, 100);
|
||||
} else if (!keepPosition && !shouldCenterRoot) {
|
||||
// 对于添加节点的情况,不居中根节点,等待后续居中新节点
|
||||
console.log('📍 跳过根节点居中,等待居中新节点');
|
||||
}
|
||||
|
||||
// Mind Elixir现在会自动使用markdown解析器渲染内容
|
||||
|
||||
} else {
|
||||
|
|
@ -404,10 +413,10 @@ const loadMindmapData = async (data, keepPosition = false) => {
|
|||
el: mindmapEl.value,
|
||||
direction: MindElixir.RIGHT,
|
||||
draggable: true,
|
||||
contextMenu: true,
|
||||
contextMenu: false,
|
||||
toolBar: true,
|
||||
nodeMenu: false,
|
||||
keypress: true,
|
||||
keypress: false, // 禁用键盘快捷键,防止不入库的添加节点操作
|
||||
autoCenter: false,
|
||||
infinite: true,
|
||||
maxScale: 5,
|
||||
|
|
@ -424,13 +433,28 @@ const loadMindmapData = async (data, keepPosition = false) => {
|
|||
const result = mindElixir.value.init(data);
|
||||
console.log('✅ Mind Elixir实例延迟创建成功');
|
||||
|
||||
// 如果有保存的位置,立即恢复位置(因为 init 会自动居中)
|
||||
if (currentPosition) {
|
||||
restorePosition(currentPosition);
|
||||
console.log('📍 延迟创建后立即恢复位置');
|
||||
|
||||
// 再次确保位置恢复
|
||||
setTimeout(() => {
|
||||
restorePosition(currentPosition);
|
||||
console.log('📍 延迟创建后二次确认位置恢复');
|
||||
}, 100);
|
||||
} else if (!keepPosition && !shouldCenterRoot) {
|
||||
// 对于添加节点的情况,不居中根节点,等待后续居中新节点
|
||||
console.log('📍 延迟创建后跳过根节点居中,等待居中新节点');
|
||||
}
|
||||
|
||||
// 延迟执行后续操作
|
||||
setTimeout(() => {
|
||||
// Mind Elixir现在会自动使用markdown解析器渲染内容
|
||||
|
||||
if (keepPosition && currentPosition) {
|
||||
if (currentPosition) {
|
||||
restorePosition(currentPosition);
|
||||
} else {
|
||||
} else if (shouldCenterRoot) {
|
||||
centerMindMap();
|
||||
}
|
||||
bindEventListeners();
|
||||
|
|
@ -451,10 +475,10 @@ const loadMindmapData = async (data, keepPosition = false) => {
|
|||
el: mindmapEl.value,
|
||||
direction: MindElixir.RIGHT,
|
||||
draggable: true,
|
||||
contextMenu: true,
|
||||
contextMenu: false,
|
||||
toolBar: true,
|
||||
nodeMenu: false,
|
||||
keypress: true,
|
||||
keypress: false, // 禁用键盘快捷键,防止不入库的添加节点操作
|
||||
autoCenter: false,
|
||||
infinite: true,
|
||||
maxScale: 5,
|
||||
|
|
@ -471,11 +495,26 @@ const loadMindmapData = async (data, keepPosition = false) => {
|
|||
const result = mindElixir.value.init(data);
|
||||
// console.log('✅ Mind Elixir实例重试创建成功');
|
||||
|
||||
// 如果有保存的位置,立即恢复位置(因为 init 会自动居中)
|
||||
if (currentPosition) {
|
||||
restorePosition(currentPosition);
|
||||
console.log('📍 重试创建后立即恢复位置');
|
||||
|
||||
// 再次确保位置恢复
|
||||
setTimeout(() => {
|
||||
restorePosition(currentPosition);
|
||||
console.log('📍 重试创建后二次确认位置恢复');
|
||||
}, 100);
|
||||
} else if (!keepPosition && !shouldCenterRoot) {
|
||||
// 对于添加节点的情况,不居中根节点,等待后续居中新节点
|
||||
console.log('📍 重试创建后跳过根节点居中,等待居中新节点');
|
||||
}
|
||||
|
||||
// 延迟执行后续操作
|
||||
setTimeout(() => {
|
||||
if (keepPosition && currentPosition) {
|
||||
if (currentPosition) {
|
||||
restorePosition(currentPosition);
|
||||
} else {
|
||||
} else if (shouldCenterRoot) {
|
||||
centerMindMap();
|
||||
}
|
||||
bindEventListeners();
|
||||
|
|
@ -494,20 +533,20 @@ const loadMindmapData = async (data, keepPosition = false) => {
|
|||
setTimeout(() => {
|
||||
if (keepPosition && currentPosition) {
|
||||
restorePosition(currentPosition);
|
||||
} else {
|
||||
} else if (shouldCenterRoot) {
|
||||
centerMindMap();
|
||||
}
|
||||
hideWelcomePage();
|
||||
bindEventListeners();
|
||||
|
||||
// 如果需要保持位置,延迟再次恢复以确保完全渲染
|
||||
if (keepPosition && currentPosition) {
|
||||
// 如果有保存的位置,延迟再次恢复以确保完全渲染
|
||||
if (currentPosition) {
|
||||
setTimeout(() => {
|
||||
restorePosition(currentPosition);
|
||||
// 添加节点描述显示
|
||||
addNodeDescriptions();
|
||||
}, 500);
|
||||
} else {
|
||||
} else if (shouldCenterRoot) {
|
||||
// 延迟再次居中,确保所有节点都渲染完成
|
||||
setTimeout(() => {
|
||||
centerMindMap();
|
||||
|
|
@ -515,6 +554,11 @@ const loadMindmapData = async (data, keepPosition = false) => {
|
|||
// 添加节点描述显示
|
||||
addNodeDescriptions();
|
||||
}, 500);
|
||||
} else {
|
||||
// 既不保持位置也不居中根节点,只添加节点描述
|
||||
setTimeout(() => {
|
||||
addNodeDescriptions();
|
||||
}, 500);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
|
|
@ -909,6 +953,14 @@ const applyCustomNodePositions = () => {
|
|||
// 计算思维导图整体位置并居中显示
|
||||
const centerMindMap = () => {
|
||||
try {
|
||||
// 使用 MindElixir 自带的 toCenter 方法实现根节点居中
|
||||
if (mindElixir.value && mindElixir.value.toCenter) {
|
||||
mindElixir.value.toCenter();
|
||||
console.log('✅ 使用 MindElixir toCenter 方法实现根节点居中');
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果 toCenter 方法不可用,使用原有的手动居中方法作为备用方案
|
||||
const mindmapContainer = mindmapEl.value;
|
||||
if (!mindmapContainer) return;
|
||||
|
||||
|
|
@ -990,9 +1042,54 @@ const centerMindMap = () => {
|
|||
|
||||
|
||||
|
||||
// 居中显示指定节点并进入编辑状态(平滑动画版本)
|
||||
const centerNodeAndEdit = async (nodeId) => {
|
||||
if (!mindElixir.value || !nodeId) return;
|
||||
|
||||
try {
|
||||
console.log('🎯 开始处理新节点:', nodeId);
|
||||
|
||||
// 多次尝试查找节点元素,减少延迟
|
||||
let topicElement = null;
|
||||
let attempts = 0;
|
||||
const maxAttempts = 5;
|
||||
|
||||
while (!topicElement && attempts < maxAttempts) {
|
||||
topicElement = mindElixir.value.findEle(nodeId);
|
||||
if (!topicElement) {
|
||||
attempts++;
|
||||
await new Promise(resolve => setTimeout(resolve, 50)); // 很短的延迟
|
||||
}
|
||||
}
|
||||
|
||||
if (topicElement) {
|
||||
console.log('✅ 找到节点元素:', topicElement);
|
||||
|
||||
// 使用平滑动画居中显示节点
|
||||
if (mindElixir.value.scrollIntoView) {
|
||||
// scrollIntoView 内部会调用 move(-offsetX, -offsetY, true) 实现平滑动画
|
||||
mindElixir.value.scrollIntoView(topicElement);
|
||||
console.log('✅ 节点已平滑居中显示');
|
||||
}
|
||||
|
||||
// 等待动画完成后进入编辑状态
|
||||
setTimeout(() => {
|
||||
if (mindElixir.value.beginEdit) {
|
||||
mindElixir.value.beginEdit(topicElement);
|
||||
console.log('✅ 节点已进入编辑状态');
|
||||
}
|
||||
}, 350); // 等待动画完成(0.3s + 50ms缓冲)
|
||||
} else {
|
||||
console.error('❌ 多次尝试后仍未找到节点元素:', nodeId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 居中显示节点失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 计算菜单位置
|
||||
const calculateMenuPosition = () => {
|
||||
// console.log("计算菜单位置,选中节点:", selectedNode.value);
|
||||
// console.log("🎯 计算菜单位置,选中节点:", selectedNode.value); // ✅ 注释掉频繁日志
|
||||
if (!selectedNode.value) return;
|
||||
|
||||
// 使用Mind Elixir的API获取节点位置
|
||||
|
|
@ -1057,28 +1154,8 @@ const calculateMenuPosition = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// 如果还是找不到,尝试查找所有可能的节点元素
|
||||
if (!nodeElement) {
|
||||
const allElements = document.querySelectorAll('*');
|
||||
for (const element of allElements) {
|
||||
if (element.textContent && element.textContent.trim() === selectedNode.value.topic) {
|
||||
// 优先选择me-tpc元素
|
||||
if (element.tagName === 'ME-TPC') {
|
||||
nodeElement = element;
|
||||
break;
|
||||
}
|
||||
// 或者选择包含me-tpc的父元素
|
||||
if (element.closest('me-tpc')) {
|
||||
nodeElement = element;
|
||||
break;
|
||||
}
|
||||
if (element.classList.contains('topic') || element.closest('.topic')) {
|
||||
nodeElement = element.closest('.topic') || element;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// ❌ 移除这个性能杀手 - 不要遍历所有元素!
|
||||
// 如果实在找不到,就使用默认位置
|
||||
|
||||
// console.log("找到节点元素:", nodeElement);
|
||||
|
||||
|
|
@ -1708,6 +1785,36 @@ const showCopyError = () => {
|
|||
}, 3000);
|
||||
};
|
||||
|
||||
// 通用通知函数
|
||||
const showNotification = (message, type = 'success') => {
|
||||
const notification = document.createElement('div');
|
||||
notification.textContent = message;
|
||||
|
||||
const bgColor = type === 'success' ? '#4CAF50' : type === 'error' ? '#f44336' : '#ff9800';
|
||||
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: ${bgColor};
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
z-index: 10000;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
animation: slideIn 0.3s ease;
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.parentNode.removeChild(notification);
|
||||
}
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
// 显示编辑成功通知
|
||||
const showEditSuccessNotification = () => {
|
||||
const notification = document.createElement('div');
|
||||
|
|
@ -1797,25 +1904,88 @@ const handleNodeMove = async (node, oldParent, newParent) => {
|
|||
}
|
||||
};
|
||||
|
||||
// 处理节点移动操作
|
||||
const handleNodeMoveOperation = async (operation) => {
|
||||
// 处理节点拖拽操作(更新节点的父子关系)
|
||||
const handleNodeDragOperation = async (operation) => {
|
||||
try {
|
||||
// 解析操作对象
|
||||
const movedNode = operation.obj; // 被移动的节点
|
||||
console.log("🎯 处理节点拖拽操作:", operation.name);
|
||||
console.log("📦 操作详情:", {
|
||||
name: operation.name,
|
||||
objs: operation.objs,
|
||||
toObj: operation.toObj
|
||||
});
|
||||
|
||||
// 延迟保存,等待MindElixir更新完DOM
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const movedNodes = operation.objs || []; // 被移动的节点数组
|
||||
const targetNode = operation.toObj; // 目标节点
|
||||
const originParentId = operation.originParentId; // 原始父节点ID
|
||||
|
||||
if (movedNode && targetNode) {
|
||||
// movedNode 是被移动的节点,targetNode 是目标父节点
|
||||
const node = movedNode;
|
||||
const newParentId = targetNode.id;
|
||||
|
||||
// 调用后端API更新节点
|
||||
await updateNodeParent(node, newParentId);
|
||||
if (!movedNodes.length || !targetNode) {
|
||||
console.warn('⚠️ 拖拽操作缺少必要信息');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`📦 准备保存 ${movedNodes.length} 个节点的父子关系`);
|
||||
|
||||
// 根据不同的移动类型确定新的父节点
|
||||
let newParentId = null;
|
||||
|
||||
if (operation.name === 'moveNodeIn') {
|
||||
// 拖入目标节点内部,目标节点成为新父节点
|
||||
newParentId = targetNode.id;
|
||||
console.log(`📌 拖入操作:新父节点为 ${newParentId}`);
|
||||
} else if (operation.name === 'moveNodeBefore' || operation.name === 'moveNodeAfter') {
|
||||
// 拖到目标节点前后,与目标节点共享同一个父节点
|
||||
newParentId = targetNode.parent?.id || null;
|
||||
console.log(`📌 拖到兄弟位置:新父节点为 ${newParentId || '根节点'}`);
|
||||
}
|
||||
|
||||
// 更新所有被移动节点的父节点
|
||||
const updatePromises = movedNodes.map(async (node) => {
|
||||
try {
|
||||
console.log(`🔄 更新节点 ${node.id} 的父节点为 ${newParentId || '根节点'}`);
|
||||
|
||||
const response = await mindmapAPI.updateNode(node.id, {
|
||||
newParentId: newParentId
|
||||
});
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
console.log(`✅ 节点 ${node.id} 父子关系更新成功`);
|
||||
return { success: true, nodeId: node.id };
|
||||
} else {
|
||||
console.warn(`⚠️ 节点 ${node.id} 父子关系更新失败:`, response);
|
||||
return { success: false, nodeId: node.id };
|
||||
}
|
||||
} catch (error) {
|
||||
// 静默处理错误
|
||||
console.error(`❌ 节点 ${node.id} 父子关系更新失败:`, error);
|
||||
return { success: false, nodeId: node.id, error };
|
||||
}
|
||||
});
|
||||
|
||||
// 等待所有更新完成
|
||||
const results = await Promise.all(updatePromises);
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
const failCount = results.filter(r => !r.success).length;
|
||||
|
||||
console.log(`📊 拖拽保存结果: ${successCount} 成功, ${failCount} 失败`);
|
||||
|
||||
// 取消弹窗提示,只在控制台记录
|
||||
// if (successCount > 0) {
|
||||
// showNotification(`✅ 节点拖拽已保存 (${successCount}/${movedNodes.length})`, 'success');
|
||||
// }
|
||||
|
||||
// if (failCount > 0) {
|
||||
// showNotification(`⚠️ 部分节点保存失败 (${failCount}/${movedNodes.length})`, 'error');
|
||||
// }
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 保存拖拽后的结构失败:', error);
|
||||
showNotification('❌ 节点拖拽保存失败', 'error');
|
||||
}
|
||||
}, 500); // 延迟500ms确保DOM更新完成
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 处理节点拖拽操作失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -1967,47 +2137,23 @@ const addChildNodeToAPI = async (parentNode) => {
|
|||
// console.log("🎯 使用loadMindmapData刷新思维导图显示新节点...");
|
||||
|
||||
try {
|
||||
// 保存当前位置
|
||||
const currentPosition = saveCurrentPosition();
|
||||
|
||||
// 延迟一下让用户看到提示
|
||||
await new Promise(resolve => setTimeout(resolve, 800));
|
||||
|
||||
// 重新获取最新的思维导图数据
|
||||
const mindmapResponse = await mindmapAPI.getMindmap(mindmapId);
|
||||
if (mindmapResponse.data && mindmapResponse.data.nodeData) {
|
||||
// 使用可靠的loadMindmapData方法,保持位置
|
||||
await loadMindmapData(mindmapResponse.data, true);
|
||||
// 使用可靠的loadMindmapData方法,不保持位置,不居中根节点(因为我们要居中新节点)
|
||||
await loadMindmapData(mindmapResponse.data, false, false);
|
||||
// console.log("✅ 思维导图刷新成功");
|
||||
|
||||
// 延迟后查找新节点并进入编辑模式
|
||||
setTimeout(async () => {
|
||||
// 立即居中显示新节点并进入编辑模式,不延迟
|
||||
try {
|
||||
// 重新获取数据确保找到新节点
|
||||
const latestResponse = await mindmapAPI.getMindmap(mindmapId);
|
||||
if (latestResponse.data && latestResponse.data.nodeData) {
|
||||
// 查找新添加的节点
|
||||
const findNewNode = (node) => {
|
||||
if (node.id === newNode.id) return node;
|
||||
if (node.children) {
|
||||
for (const child of node.children) {
|
||||
const found = findNewNode(child);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const foundNode = findNewNode(latestResponse.data.nodeData);
|
||||
if (foundNode && mindElixir.value) {
|
||||
// console.log("找到新节点,进入编辑模式:", foundNode);
|
||||
mindElixir.value.editText(foundNode);
|
||||
}
|
||||
}
|
||||
console.log('🎯 开始居中显示新子节点:', newNode.id);
|
||||
await centerNodeAndEdit(newNode.id);
|
||||
} catch (error) {
|
||||
console.error("查找新节点失败:", error);
|
||||
console.error("居中显示新节点失败:", error);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
} else {
|
||||
throw new Error("无法获取思维导图数据");
|
||||
|
|
@ -2071,46 +2217,20 @@ const addSiblingNodeToAPI = async (node) => {
|
|||
console.log("🎯 使用MindElixir init方法重新初始化数据...");
|
||||
|
||||
try {
|
||||
// 保存当前位置
|
||||
const currentPosition = saveCurrentPosition();
|
||||
|
||||
// 重新获取最新的思维导图数据
|
||||
const mindmapResponse = await mindmapAPI.getMindmap(mindmapId);
|
||||
if (mindmapResponse.data && mindmapResponse.data.nodeData) {
|
||||
// 使用可靠的loadMindmapData方法,保持位置
|
||||
await loadMindmapData(mindmapResponse.data, true);
|
||||
// 使用可靠的loadMindmapData方法,不保持位置,不居中根节点(因为我们要居中新节点)
|
||||
await loadMindmapData(mindmapResponse.data, false, false);
|
||||
console.log("✅ 思维导图刷新成功");
|
||||
|
||||
|
||||
|
||||
// 延迟后查找新节点并进入编辑模式
|
||||
setTimeout(async () => {
|
||||
// 立即居中显示新节点并进入编辑模式,不延迟
|
||||
try {
|
||||
// 重新获取数据确保找到新节点
|
||||
const latestResponse = await mindmapAPI.getMindmap(mindmapId);
|
||||
if (latestResponse.data && latestResponse.data.nodeData) {
|
||||
// 查找新添加的节点
|
||||
const findNewNode = (node) => {
|
||||
if (node.id === newNode.id) return node;
|
||||
if (node.children) {
|
||||
for (const child of node.children) {
|
||||
const found = findNewNode(child);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const foundNode = findNewNode(latestResponse.data.nodeData);
|
||||
if (foundNode && mindElixir.value) {
|
||||
console.log("找到新节点,进入编辑模式:", foundNode);
|
||||
mindElixir.value.editText(foundNode);
|
||||
}
|
||||
}
|
||||
console.log('🎯 开始居中显示新兄弟节点:', newNode.id);
|
||||
await centerNodeAndEdit(newNode.id);
|
||||
} catch (error) {
|
||||
console.error("查找新节点失败:", error);
|
||||
console.error("居中显示新节点失败:", error);
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
throw new Error("无法获取思维导图数据");
|
||||
}
|
||||
|
|
@ -2121,7 +2241,7 @@ const addSiblingNodeToAPI = async (node) => {
|
|||
// 最终回退方案:完整重新加载
|
||||
const mindmapResponse = await mindmapAPI.getMindmap(mindmapId);
|
||||
if (mindmapResponse.data && mindmapResponse.data.nodeData) {
|
||||
await loadMindmapData(mindmapResponse.data, true);
|
||||
await loadMindmapData(mindmapResponse.data, true, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2145,26 +2265,11 @@ const deleteNodeFromAPI = async (node) => {
|
|||
// 获取思维导图ID
|
||||
const mindmapId = currentMindmapId.value || node.mindmap_id || node.mindmapId;
|
||||
if (mindmapId) {
|
||||
// 重新加载数据,保持当前位置
|
||||
// 重新加载数据,保持当前位置,不居中根节点
|
||||
const mindmapResponse = await mindmapAPI.getMindmap(mindmapId);
|
||||
if (mindmapResponse.data && mindmapResponse.data.nodeData) {
|
||||
await loadMindmapData(mindmapResponse.data, true); // 保持当前位置
|
||||
|
||||
|
||||
|
||||
// 延迟后重新加载思维导图数据,保持当前位置
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
console.log("重新加载思维导图数据...");
|
||||
const refreshResponse = await mindmapAPI.getMindmap(mindmapId);
|
||||
if (refreshResponse.data && refreshResponse.data.nodeData) {
|
||||
await loadMindmapData(refreshResponse.data, true); // 保持当前位置
|
||||
console.log("思维导图数据重新加载成功");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("重新加载思维导图失败:", error);
|
||||
}
|
||||
}, 1500);
|
||||
await loadMindmapData(mindmapResponse.data, true, false); // 保持当前位置,不居中根节点
|
||||
console.log("✅ 删除节点后思维导图已刷新,保持当前位置");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2273,13 +2378,25 @@ const createNodesRecursively = async (node, mindmapId, parentId) => {
|
|||
};
|
||||
|
||||
// 绑定事件监听器
|
||||
// 保存事件监听器引用,用于清理
|
||||
let wheelHandler = null;
|
||||
let clickHandler = null;
|
||||
|
||||
const bindEventListeners = () => {
|
||||
if (!mindElixir.value) return;
|
||||
|
||||
console.log("绑定事件监听器...");
|
||||
|
||||
// ✅ 先移除旧的事件监听器,避免重复绑定
|
||||
if (wheelHandler) {
|
||||
mindmapEl.value.removeEventListener('wheel', wheelHandler);
|
||||
}
|
||||
if (clickHandler) {
|
||||
mindmapEl.value.removeEventListener('click', clickHandler);
|
||||
}
|
||||
|
||||
// 添加鼠标滚轮缩放功能
|
||||
mindmapEl.value.addEventListener('wheel', (event) => {
|
||||
wheelHandler = (event) => {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
event.preventDefault();
|
||||
|
||||
|
|
@ -2296,27 +2413,27 @@ const bindEventListeners = () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
mindmapEl.value.addEventListener('wheel', wheelHandler);
|
||||
|
||||
// 重新绑定事件监听器
|
||||
mindElixir.value.bus.addListener("select", (node) => {
|
||||
console.log("select事件触发:", node);
|
||||
// console.log("select事件触发:", node); // ✅ 注释掉频繁日志
|
||||
selectedNode.value = node;
|
||||
// 延迟计算菜单位置,确保DOM已更新
|
||||
setTimeout(() => {
|
||||
if (!contextMenuStyle.value.left || contextMenuStyle.value.left === '50%') {
|
||||
calculateMenuPosition();
|
||||
}
|
||||
}, 200);
|
||||
}, 50);
|
||||
});
|
||||
|
||||
mindElixir.value.bus.addListener("selectNode", (node) => {
|
||||
console.log("selectNode事件触发:", node);
|
||||
// console.log("selectNode事件触发:", node); // ✅ 注释掉频繁日志
|
||||
selectedNode.value = node;
|
||||
// 延迟计算菜单位置,确保DOM已更新
|
||||
setTimeout(() => {
|
||||
if (!contextMenuStyle.value.left || contextMenuStyle.value.left === '50%') {
|
||||
calculateMenuPosition();
|
||||
}
|
||||
}, 200);
|
||||
}, 50);
|
||||
});
|
||||
|
||||
// 监听缩放变化,保持缩放状态
|
||||
|
|
@ -2329,19 +2446,24 @@ const bindEventListeners = () => {
|
|||
}
|
||||
});
|
||||
|
||||
// 添加定时器来持续保持缩放状态
|
||||
// 添加定时器来持续保持缩放状态(降低频率)
|
||||
// 清理旧的定时器,避免重复
|
||||
if (window.zoomIntervalId) {
|
||||
clearInterval(window.zoomIntervalId);
|
||||
}
|
||||
|
||||
const zoomInterval = setInterval(() => {
|
||||
if (zoomLevel.value !== 1 && mindmapEl.value) {
|
||||
maintainZoomLevel();
|
||||
}
|
||||
}, 100);
|
||||
}, 500); // ✅ 改为500ms,降低CPU占用
|
||||
|
||||
// 保存定时器ID,以便后续清理
|
||||
window.zoomIntervalId = zoomInterval;
|
||||
|
||||
// 添加点击事件监听器
|
||||
let lastClickTime = 0;
|
||||
mindmapEl.value.addEventListener('click', (event) => {
|
||||
clickHandler = (event) => {
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - lastClickTime < 100) {
|
||||
return;
|
||||
|
|
@ -2349,21 +2471,33 @@ const bindEventListeners = () => {
|
|||
lastClickTime = currentTime;
|
||||
|
||||
const clickedElement = event.target;
|
||||
const isNodeClick = clickedElement.closest('me-tpc') ||
|
||||
const topicElement = clickedElement.closest('me-tpc') ||
|
||||
clickedElement.closest('.topic') ||
|
||||
clickedElement.classList.contains('topic') ||
|
||||
clickedElement.tagName === 'ME-TPC';
|
||||
(clickedElement.classList.contains('topic') ? clickedElement : null) ||
|
||||
(clickedElement.tagName === 'ME-TPC' ? clickedElement : null);
|
||||
|
||||
if (selectedNode.value && isNodeClick) {
|
||||
if (topicElement) {
|
||||
// 点击了节点,获取节点对象并设置为选中
|
||||
const nodeObj = topicElement.nodeObj;
|
||||
if (nodeObj) {
|
||||
selectedNode.value = nodeObj;
|
||||
|
||||
// 计算菜单位置(在节点下方居中)
|
||||
const containerRect = mindmapEl.value.getBoundingClientRect();
|
||||
const left = event.clientX - containerRect.left;
|
||||
const top = event.clientY - containerRect.top + 50;
|
||||
const topicRect = topicElement.getBoundingClientRect();
|
||||
// ✅ 设置为节点中心位置,配合 CSS 的 transform: translateX(-50%) 实现居中
|
||||
const left = topicRect.left - containerRect.left + topicRect.width / 2;
|
||||
const top = topicRect.bottom - containerRect.top + 8; // 节点下方 8px
|
||||
|
||||
contextMenuStyle.value = {
|
||||
left: `${left}px`,
|
||||
top: `${top}px`
|
||||
};
|
||||
} else if (!isNodeClick) {
|
||||
|
||||
// console.log('✅ 节点被点击,显示自定义菜单:', nodeObj.topic); // ✅ 注释掉频繁日志
|
||||
}
|
||||
} else {
|
||||
// 点击空白区域,隐藏菜单
|
||||
selectedNode.value = null;
|
||||
// 点击空白区域时也关闭AI询问框
|
||||
if (showAIDialog.value) {
|
||||
|
|
@ -2373,7 +2507,9 @@ const bindEventListeners = () => {
|
|||
isAIProcessing.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
mindmapEl.value.addEventListener('click', clickHandler);
|
||||
|
||||
mindElixir.value.bus.addListener("edit", (node) => {
|
||||
console.log("edit事件触发:", node);
|
||||
|
|
@ -2385,28 +2521,30 @@ const bindEventListeners = () => {
|
|||
handleEditFinish(operation);
|
||||
});
|
||||
|
||||
// 监听操作事件 - 这是正确的方式
|
||||
// 监听操作事件 - 监听拖拽和编辑操作
|
||||
mindElixir.value.bus.addListener("operation", (operation) => {
|
||||
console.log("Mind Elixir操作事件:", operation);
|
||||
|
||||
if (operation.name === 'moveNode') {
|
||||
console.log("检测到节点移动操作:", operation);
|
||||
handleNodeMoveOperation(operation);
|
||||
// 监听拖拽移动节点事件
|
||||
if (operation.name === 'moveNodeIn' || operation.name === 'moveNodeBefore' || operation.name === 'moveNodeAfter') {
|
||||
console.log("检测到节点拖拽操作:", operation.name, operation);
|
||||
handleNodeDragOperation(operation);
|
||||
} else if (operation.name === 'finishEdit') {
|
||||
console.log("检测到编辑完成操作:", operation);
|
||||
handleEditFinish(operation);
|
||||
}
|
||||
});
|
||||
|
||||
mindElixir.value.bus.addListener("addChild", (node) => {
|
||||
console.log("添加子节点:", node);
|
||||
addChildNodeToAPI(node);
|
||||
});
|
||||
// 禁用 MindElixir 内置的添加节点事件监听,因为我们使用自定义的右键菜单
|
||||
// mindElixir.value.bus.addListener("addChild", (node) => {
|
||||
// console.log("添加子节点:", node);
|
||||
// addChildNodeToAPI(node);
|
||||
// });
|
||||
|
||||
mindElixir.value.bus.addListener("addSibling", (node) => {
|
||||
console.log("添加兄弟节点:", node);
|
||||
addSiblingNodeToAPI(node);
|
||||
});
|
||||
// mindElixir.value.bus.addListener("addSibling", (node) => {
|
||||
// console.log("添加兄弟节点:", node);
|
||||
// addSiblingNodeToAPI(node);
|
||||
// });
|
||||
|
||||
mindElixir.value.bus.addListener("removeNode", (node) => {
|
||||
console.log("删除节点:", node);
|
||||
|
|
@ -2738,8 +2876,8 @@ const refreshMindMap = async () => {
|
|||
if (response.data && response.data.nodeData) {
|
||||
console.log("✅ 获取到最新数据,开始刷新显示...");
|
||||
|
||||
// 使用loadMindmapData重新加载,保持当前位置
|
||||
await loadMindmapData(response.data, true);
|
||||
// 使用loadMindmapData重新加载,保持当前位置,不居中根节点
|
||||
await loadMindmapData(response.data, true, false);
|
||||
|
||||
console.log("🎉 思维导图刷新完成!");
|
||||
} else {
|
||||
|
|
@ -3091,10 +3229,25 @@ onMounted(async () => {
|
|||
|
||||
// 清理定时器
|
||||
const cleanupIntervals = () => {
|
||||
// 清理定时器
|
||||
if (window.zoomIntervalId) {
|
||||
clearInterval(window.zoomIntervalId);
|
||||
window.zoomIntervalId = null;
|
||||
}
|
||||
|
||||
// 清理事件监听器
|
||||
if (mindmapEl.value) {
|
||||
if (wheelHandler) {
|
||||
mindmapEl.value.removeEventListener('wheel', wheelHandler);
|
||||
wheelHandler = null;
|
||||
}
|
||||
if (clickHandler) {
|
||||
mindmapEl.value.removeEventListener('click', clickHandler);
|
||||
clickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ 已清理所有定时器和事件监听器');
|
||||
};
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
|
|
@ -3142,7 +3295,7 @@ const refreshMindmapAfterEdit = async (mindmapId, savedPosition) => {
|
|||
try {
|
||||
const mindmapResponse = await mindmapAPI.getMindmap(mindmapId);
|
||||
if (mindmapResponse.data && mindmapResponse.data.nodeData) {
|
||||
await loadMindmapData(mindmapResponse.data, true);
|
||||
await loadMindmapData(mindmapResponse.data, true, false);
|
||||
console.log("✅ 使用回退方案刷新成功");
|
||||
}
|
||||
} catch (fallbackError) {
|
||||
|
|
@ -3332,10 +3485,10 @@ const updateMindMapRealtime = async (data, title) => {
|
|||
el: mindmapEl.value,
|
||||
direction: MindElixir.RIGHT,
|
||||
draggable: true,
|
||||
contextMenu: true,
|
||||
contextMenu: false,
|
||||
toolBar: true,
|
||||
nodeMenu: false,
|
||||
keypress: true,
|
||||
nodeMenu: true,
|
||||
keypress: false, // 禁用键盘快捷键,防止不入库的添加节点操作
|
||||
autoCenter: false,
|
||||
infinite: true,
|
||||
maxScale: 5,
|
||||
|
|
@ -3445,78 +3598,6 @@ const updateMindMapRealtime = async (data, title) => {
|
|||
overflow: visible;
|
||||
}
|
||||
|
||||
/* 缩放控制按钮样式 */
|
||||
.zoom-controls {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.zoom-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.zoom-btn:hover {
|
||||
background: #f5f5f5;
|
||||
border-color: #ccc;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.zoom-btn:active {
|
||||
background: #e0e0e0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.zoom-level {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
min-width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 欢迎模式下的缩放控制按钮样式 */
|
||||
.zoom-controls.welcome-mode {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(102, 8, 116, 0.1);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.zoom-controls.welcome-mode .zoom-btn {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-color: rgba(102, 8, 116, 0.2);
|
||||
color: #660874;
|
||||
}
|
||||
|
||||
.zoom-controls.welcome-mode .zoom-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-color: rgba(102, 8, 116, 0.3);
|
||||
color: #5a0666;
|
||||
}
|
||||
|
||||
.zoom-controls.welcome-mode .zoom-level {
|
||||
color: #660874;
|
||||
}
|
||||
|
||||
/* 保存控制按钮样式 */
|
||||
.save-controls {
|
||||
position: absolute;
|
||||
|
|
|
|||
Binary file not shown.
Loading…
Reference in New Issue