2025-09-04 05:47:42 +00:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="mindmap-container">
|
|
|
|
|
|
<!-- 欢迎页面 -->
|
|
|
|
|
|
<div v-if="showWelcome" class="welcome-page">
|
|
|
|
|
|
<div class="welcome-content" :class="{ 'ai-sidebar-collapsed': isAISidebarCollapsed }">
|
|
|
|
|
|
<div class="welcome-header">
|
|
|
|
|
|
<h1>🧠 思维导图工具</h1>
|
|
|
|
|
|
<p class="welcome-subtitle">可视化您的想法,构建知识体系</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="welcome-features">
|
|
|
|
|
|
<div class="feature-item">
|
|
|
|
|
|
<div class="feature-icon">🎯</div>
|
|
|
|
|
|
<div class="feature-text">
|
|
|
|
|
|
<h3>智能AI生成</h3>
|
|
|
|
|
|
<p>上传文档,AI自动生成思维导图结构</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="feature-item">
|
|
|
|
|
|
<div class="feature-icon">✏️</div>
|
|
|
|
|
|
<div class="feature-text">
|
|
|
|
|
|
<h3>灵活编辑</h3>
|
|
|
|
|
|
<p>拖拽节点,自由调整思维导图布局</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="feature-item">
|
|
|
|
|
|
<div class="feature-icon">💾</div>
|
|
|
|
|
|
<div class="feature-text">
|
|
|
|
|
|
<h3>云端存储</h3>
|
|
|
|
|
|
<p>自动保存,多设备同步访问</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="welcome-tips">
|
|
|
|
|
|
<p>💡 提示:使用左侧AI侧边栏可以快速生成思维导图内容</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-08 07:06:08 +00:00
|
|
|
|
<!-- AI输入区域 -->
|
|
|
|
|
|
<div v-if="showAIDialog" class="ai-input-area" :style="aiInputStyle">
|
|
|
|
|
|
<div class="ai-input-header">
|
|
|
|
|
|
<span class="ai-input-title">🤖 询问AI</span>
|
|
|
|
|
|
<button @click="closeAIDialog" class="ai-close-btn">×</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="ai-input-content">
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
v-model="aiQuestion"
|
|
|
|
|
|
placeholder="请输入您的问题..."
|
|
|
|
|
|
rows="2"
|
|
|
|
|
|
:disabled="isAIProcessing"
|
|
|
|
|
|
@keydown.enter.exact="handleEnterKey"
|
|
|
|
|
|
@keydown.enter.ctrl="submitAIQuestion"
|
|
|
|
|
|
@keydown.enter.meta="submitAIQuestion"
|
|
|
|
|
|
></textarea>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="ai-input-actions">
|
|
|
|
|
|
<button @click="closeAIDialog" class="btn-cancel" :disabled="isAIProcessing">
|
|
|
|
|
|
取消
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button @click="submitAIQuestion" class="btn-submit" :disabled="!aiQuestion.trim() || isAIProcessing">
|
|
|
|
|
|
<span v-if="isAIProcessing">AI思考中...</span>
|
|
|
|
|
|
<span v-else>询问AI</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-04 05:47:42 +00:00
|
|
|
|
<!-- 思维导图容器 -->
|
2025-09-08 07:06:08 +00:00
|
|
|
|
<div v-if="!showWelcome" ref="mindmapEl" class="mindmap-el"></div>
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
<!-- 保存和刷新按钮 -->
|
|
|
|
|
|
<div v-if="!showWelcome" class="save-controls">
|
2025-10-09 06:20:51 +00:00
|
|
|
|
<button @click="saveMindMap" class="save-btn" title="保存思维导图" style="display: none;">
|
2025-09-04 05:47:42 +00:00
|
|
|
|
<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>
|
|
|
|
|
|
<polyline points="7,3 7,8 15,8"></polyline>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<span>保存</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
2025-10-09 06:20:51 +00:00
|
|
|
|
<button @click="refreshMindMap" class="refresh-btn" title="刷新思维导图" style="display: none;">
|
2025-09-04 05:47:42 +00:00
|
|
|
|
<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>
|
|
|
|
|
|
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"></path>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<span>刷新</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 悬停菜单 -->
|
|
|
|
|
|
<div v-if="selectedNode" class="context-menu" :style="contextMenuStyle">
|
|
|
|
|
|
<div class="context-menu-item" @click="addSiblingNode" title="Add a sibling card" v-if="selectedNode.parentId || selectedNode.parent">
|
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 18 18" fill="none">
|
|
|
|
|
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.9001 4.5C9.9001 5.74264 8.89274 6.75 7.6501 6.75C6.64263 6.75 5.78981 6.08785 5.5031 5.175H2.7001V11.025H5.4001V9.9C5.4001 8.90589 6.11644 8.1 7.0001 8.1H15.0501C15.9338 8.1 16.6501 8.90589 16.6501 9.9V13.95C16.6501 14.9441 15.9338 15.75 15.0501 15.75H7.0001C6.11644 15.75 5.4001 14.9441 5.4001 13.95V12.375H2.2501C1.75304 12.375 1.3501 11.7471 1.3501 11.475V4.725C1.3501 4.22794 1.75304 3.825 2.2501 3.825H5.5031C5.78981 2.91215 6.64263 2.25 7.6501 2.25C8.89274 2.25 9.9001 3.25736 9.9001 4.5ZM15.0501 9.9H7.0001V13.95H15.0501V9.9Z" fill="currentColor" fill-opacity="1"></path>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="context-menu-item" @click="addChildNode" title="Add a child card">
|
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 18 18" fill="none">
|
|
|
|
|
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.9501 6.63691H8.35014V11.3619H15.9501V6.63691ZM8.35014 4.83691C7.46649 4.83691 6.75014 5.6428 6.75014 6.63691V8.32441H4.84719C4.56048 7.41157 3.70766 6.74941 2.7002 6.74941C1.45755 6.74941 0.450195 7.75677 0.450195 8.99941C0.450195 10.2421 1.45755 11.2494 2.7002 11.2494C3.70766 11.2494 4.56048 10.5873 4.84719 9.67441H6.75014V11.3619C6.75014 12.356 7.46649 13.1619 8.35014 13.1619H15.9501C16.8338 13.1619 17.5501 12.356 17.5501 11.3619V6.63691C17.5501 5.6428 16.8338 4.83691 15.9501 4.83691H8.35014Z" fill="currentColor" fill-opacity="1"></path>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="context-menu-item" @click="copyNodeText" title="Copy text">
|
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 18 18" fill="none">
|
|
|
|
|
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.4503 2.99952C16.4016 2.18908 15.7289 1.54688 14.9062 1.54688H6.46875L6.37452 1.5497C5.56408 1.5984 4.92188 2.27108 4.92188 3.09375V3.54688C4.92188 3.68495 5.0338 3.79688 5.17188 3.79688H6.35938C6.49745 3.79688 6.60938 3.68495 6.60938 3.54688V3.375L6.61309 3.34276C6.62766 3.28063 6.68343 3.23438 6.75 3.23438H14.625L14.6572 3.23809C14.7261 3.23438 14.7656 3.30029 14.7656 3.375V11.25L14.7619 11.2822C14.7473 11.3444 14.6916 11.3906 14.625 11.3906H14.4531C14.3151 11.3906 14.2031 11.5026 14.2031 11.6406V12.8281C14.2031 12.9662 14.3151 13.0781 14.4531 13.0781H14.9062L15.0005 13.0753C15.8109 13.0266 16.4531 12.3539 16.4531 11.5312V3.09375L16.4503 2.99952ZM11.5312 4.92188H3.09375C2.23943 4.92188 1.54688 5.61443 1.54688 6.46875V14.9062C1.54688 15.7606 2.23943 16.4531 3.09375 16.4531H11.5312C12.3856 16.4531 13.0781 15.7606 13.0781 14.9062V6.46875C13.0781 5.61443 12.3856 4.92188 11.5312 4.92188ZM3.37032 6.615H11.2635C11.3361 6.615 11.395 6.6739 11.395 6.74655V14.6397C11.395 14.7124 11.3361 14.7712 11.2635 14.7712H3.37032C3.29767 14.7712 3.23877 14.7124 3.23877 14.6397V6.74655C3.23877 6.6739 3.29767 6.615 3.37032 6.615ZM4.5 8.5C4.5 8.27909 4.67909 8.1 4.9 8.1H9.725C9.94591 8.1 10.125 8.27909 10.125 8.5V9.5C10.125 9.72091 9.94591 9.9 9.725 9.9H4.9C4.67909 9.9 4.5 9.72091 4.5 9.5V8.5ZM4.9 11.475C4.67909 11.475 4.5 11.6541 4.5 11.875V12.875C4.5 13.0959 4.67909 13.275 4.9 13.275H9.725C9.94591 13.275 10.125 13.0959 10.125 12.875V11.875C10.125 11.6541 9.94591 11.475 9.725 11.475H4.9Z" fill="currentColor" fill-opacity="1"></path>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</div>
|
2025-09-08 07:06:08 +00:00
|
|
|
|
|
|
|
|
|
|
<!-- Ask AI 功能 -->
|
|
|
|
|
|
<div class="context-menu-item ask-ai" @click="askAIForNode" title="Ask AI">
|
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
|
|
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
|
|
|
|
|
<path d="M13 8H7"/>
|
|
|
|
|
|
<path d="M17 12H7"/>
|
|
|
|
|
|
<circle cx="17" cy="8" r="1"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-04 05:47:42 +00:00
|
|
|
|
<div class="context-menu-item delete" @click="deleteSelectedNode" title="Delete">
|
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
|
|
|
|
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z" fill="currentColor"/>
|
|
|
|
|
|
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z" fill="currentColor"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-10-10 18:39:23 +00:00
|
|
|
|
<!-- 图片预览模态框 -->
|
|
|
|
|
|
<div v-if="showImagePreview" class="image-preview-modal" @click="closeImagePreview">
|
|
|
|
|
|
<div class="image-preview-content" @click.stop>
|
|
|
|
|
|
<div class="image-preview-header">
|
|
|
|
|
|
<span class="image-preview-title">{{ imagePreviewTitle }}</span>
|
|
|
|
|
|
<button @click="closeImagePreview" class="image-preview-close">×</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="image-preview-body">
|
|
|
|
|
|
<div v-if="imagePreviewLoading" class="image-preview-loading">
|
|
|
|
|
|
<div class="loading-spinner"></div>
|
|
|
|
|
|
<p>加载图片中...</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else-if="imagePreviewError" class="image-preview-error">
|
|
|
|
|
|
<p>❌ 图片加载失败</p>
|
|
|
|
|
|
<p>{{ imagePreviewError }}</p>
|
|
|
|
|
|
<button @click="retryLoadImage" class="retry-button">重试</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<img
|
|
|
|
|
|
v-else
|
|
|
|
|
|
:src="imagePreviewUrl"
|
|
|
|
|
|
:alt="imagePreviewTitle"
|
|
|
|
|
|
class="preview-image"
|
|
|
|
|
|
@load="onImageLoad"
|
|
|
|
|
|
@error="onImageError"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
2025-10-11 04:51:05 +00:00
|
|
|
|
|
2025-09-04 05:47:42 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
|
2025-09-10 07:27:37 +00:00
|
|
|
|
import MindElixir from '../lib/mind-elixir/dist/MindElixir.js';
|
|
|
|
|
|
import '../lib/mind-elixir/dist/style.css';
|
2025-10-10 10:16:25 +00:00
|
|
|
|
|
2025-10-11 04:51:05 +00:00
|
|
|
|
// 自定义主题
|
2025-10-10 10:16:25 +00:00
|
|
|
|
const customTheme = {
|
|
|
|
|
|
name: 'Light Purple',
|
|
|
|
|
|
type: 'light',
|
|
|
|
|
|
palette: [
|
|
|
|
|
|
'#660874', // 主题紫色 - 所有线条都使用这个颜色
|
|
|
|
|
|
'#660874', // 统一颜色
|
|
|
|
|
|
'#660874', // 统一颜色
|
|
|
|
|
|
'#660874', // 统一颜色
|
|
|
|
|
|
'#660874', // 统一颜色
|
|
|
|
|
|
'#660874', // 统一颜色
|
|
|
|
|
|
'#660874', // 统一颜色
|
|
|
|
|
|
'#660874', // 统一颜色
|
|
|
|
|
|
'#660874', // 统一颜色
|
|
|
|
|
|
'#660874' // 统一颜色
|
|
|
|
|
|
],
|
|
|
|
|
|
cssVar: {
|
|
|
|
|
|
'--node-gap-x': '30px',
|
|
|
|
|
|
'--node-gap-y': '10px',
|
|
|
|
|
|
'--main-gap-x': '65px',
|
|
|
|
|
|
'--main-gap-y': '45px',
|
|
|
|
|
|
'--root-radius': '30px',
|
|
|
|
|
|
'--main-radius': '20px',
|
|
|
|
|
|
'--root-color': '#ffffff',
|
|
|
|
|
|
'--root-bgcolor': '#660874', // 根节点背景用主题紫色
|
|
|
|
|
|
'--root-border-color': 'rgba(0, 0, 0, 0)',
|
|
|
|
|
|
'--main-color': '#444446',
|
|
|
|
|
|
'--main-bgcolor': '#ffffff',
|
|
|
|
|
|
'--topic-padding': '3px',
|
|
|
|
|
|
'--color': '#777777',
|
|
|
|
|
|
'--bgcolor': '#f6f6f6',
|
|
|
|
|
|
'--selected': '#660874', // 选中状态用主题紫色
|
|
|
|
|
|
'--accent-color': '#660874', // 强调色用主题紫色
|
|
|
|
|
|
'--panel-color': '#444446',
|
|
|
|
|
|
'--panel-bgcolor': '#ffffff',
|
|
|
|
|
|
'--panel-border-color': '#eaeaea',
|
|
|
|
|
|
'--map-padding': '50px',
|
2025-10-11 04:51:05 +00:00
|
|
|
|
// 节点边框相关配置 - 默认启用封闭边框
|
|
|
|
|
|
'--enable-node-border': 'true',
|
|
|
|
|
|
'--node-border-padding': '8',
|
|
|
|
|
|
'--node-border-radius': '8',
|
|
|
|
|
|
'--node-border-color': '#660874',
|
|
|
|
|
|
'--node-border-fill': '#ffffff',
|
|
|
|
|
|
'--node-border-width': '1',
|
2025-10-10 10:16:25 +00:00
|
|
|
|
},
|
|
|
|
|
|
};
|
2025-09-04 05:47:42 +00:00
|
|
|
|
import { mindmapAPI } from '../api/mindmap.js';
|
2025-09-10 05:02:45 +00:00
|
|
|
|
import {
|
|
|
|
|
|
smartRenderNodeContent,
|
|
|
|
|
|
hasMarkdownSyntax,
|
|
|
|
|
|
renderMarkdownToHTML
|
|
|
|
|
|
} from '../utils/markdownRenderer.js';
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
// 响应式数据
|
|
|
|
|
|
const mindmapEl = ref(null);
|
|
|
|
|
|
const mindElixir = ref(null);
|
|
|
|
|
|
const selectedNode = ref(null);
|
|
|
|
|
|
const contextMenuStyle = ref({});
|
2025-09-08 07:06:08 +00:00
|
|
|
|
const aiInputStyle = ref({});
|
2025-09-04 05:47:42 +00:00
|
|
|
|
const currentMindmapId = ref(null); // 存储当前思维导图ID
|
|
|
|
|
|
const zoomLevel = ref(1); // 缩放级别
|
|
|
|
|
|
const showWelcome = ref(true); // 控制欢迎页面显示
|
|
|
|
|
|
const isAISidebarCollapsed = ref(false); // AI侧边栏折叠状态
|
|
|
|
|
|
|
2025-09-08 07:06:08 +00:00
|
|
|
|
// Ask AI 相关状态
|
|
|
|
|
|
const showAIDialog = ref(false); // 控制AI对话框显示
|
|
|
|
|
|
const currentQuestionNode = ref(null); // 当前询问的节点
|
|
|
|
|
|
const aiQuestion = ref(''); // 用户输入的问题
|
|
|
|
|
|
const isAIProcessing = ref(false); // AI处理状态
|
|
|
|
|
|
|
2025-09-04 05:47:42 +00:00
|
|
|
|
// 节点拖拽状态管理
|
|
|
|
|
|
const nodePositions = ref(new Map()); // 存储节点位置偏移
|
|
|
|
|
|
const isDragging = ref(false);
|
|
|
|
|
|
const dragStartPos = ref({ x: 0, y: 0 });
|
|
|
|
|
|
const currentNodeId = ref(null);
|
|
|
|
|
|
|
2025-10-10 18:39:23 +00:00
|
|
|
|
// 图片预览相关状态
|
|
|
|
|
|
const showImagePreview = ref(false);
|
|
|
|
|
|
const imagePreviewUrl = ref('');
|
|
|
|
|
|
const imagePreviewTitle = ref('');
|
|
|
|
|
|
const imagePreviewLoading = ref(false);
|
|
|
|
|
|
const imagePreviewError = ref('');
|
|
|
|
|
|
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
|
2025-10-11 04:51:05 +00:00
|
|
|
|
|
2025-09-04 05:47:42 +00:00
|
|
|
|
// 显示欢迎页面
|
|
|
|
|
|
const showWelcomePage = () => {
|
|
|
|
|
|
showWelcome.value = true;
|
|
|
|
|
|
// 清空思维导图容器
|
|
|
|
|
|
if (mindmapEl.value) {
|
|
|
|
|
|
mindmapEl.value.innerHTML = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
// 重置Mind Elixir实例
|
|
|
|
|
|
if (mindElixir.value) {
|
|
|
|
|
|
mindElixir.value = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 隐藏欢迎页面,显示思维导图
|
|
|
|
|
|
const hideWelcomePage = () => {
|
|
|
|
|
|
showWelcome.value = false;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-08 10:20:48 +00:00
|
|
|
|
// 显示思维导图页面(供父组件调用)
|
|
|
|
|
|
const showMindMapPage = () => {
|
|
|
|
|
|
showWelcome.value = false;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-10 18:39:23 +00:00
|
|
|
|
// 图片预览相关方法
|
|
|
|
|
|
const openImagePreview = (imageUrl, altText = '') => {
|
|
|
|
|
|
console.log('🖼️ 打开图片预览:', { imageUrl, altText });
|
|
|
|
|
|
|
|
|
|
|
|
// 处理图片URL,确保是有效的URL
|
|
|
|
|
|
let processedUrl = imageUrl;
|
|
|
|
|
|
if (typeof imageUrl === 'string') {
|
|
|
|
|
|
// 如果是相对路径,转换为绝对路径
|
|
|
|
|
|
if (imageUrl.startsWith('/') || imageUrl.startsWith('./') || imageUrl.startsWith('../')) {
|
|
|
|
|
|
processedUrl = new URL(imageUrl, window.location.origin).href;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 如果是base64图片,直接使用
|
|
|
|
|
|
else if (imageUrl.startsWith('data:image/')) {
|
|
|
|
|
|
processedUrl = imageUrl;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 如果是完整URL,直接使用
|
|
|
|
|
|
else if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
|
|
|
|
|
|
processedUrl = imageUrl;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log('🖼️ 处理后的图片URL:', processedUrl);
|
|
|
|
|
|
|
|
|
|
|
|
showImagePreview.value = true;
|
|
|
|
|
|
imagePreviewTitle.value = altText || '图片预览';
|
|
|
|
|
|
imagePreviewLoading.value = true;
|
|
|
|
|
|
imagePreviewError.value = '';
|
|
|
|
|
|
|
|
|
|
|
|
// 启动超时机制
|
|
|
|
|
|
startImageLoadTimeout();
|
|
|
|
|
|
|
|
|
|
|
|
// 预加载图片,确保URL有效
|
|
|
|
|
|
const img = new Image();
|
|
|
|
|
|
img.onload = () => {
|
|
|
|
|
|
console.log('✅ 图片预加载成功');
|
|
|
|
|
|
// 预加载成功后,直接设置图片URL并停止加载状态
|
|
|
|
|
|
imagePreviewUrl.value = processedUrl;
|
|
|
|
|
|
imagePreviewLoading.value = false;
|
|
|
|
|
|
clearImageLoadTimeout();
|
|
|
|
|
|
};
|
|
|
|
|
|
img.onerror = () => {
|
|
|
|
|
|
console.error('❌ 图片预加载失败:', processedUrl);
|
|
|
|
|
|
clearImageLoadTimeout();
|
|
|
|
|
|
imagePreviewLoading.value = false;
|
|
|
|
|
|
imagePreviewError.value = `图片加载失败: ${processedUrl}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
img.src = processedUrl;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const closeImagePreview = () => {
|
|
|
|
|
|
clearImageLoadTimeout();
|
|
|
|
|
|
showImagePreview.value = false;
|
|
|
|
|
|
imagePreviewUrl.value = '';
|
|
|
|
|
|
imagePreviewTitle.value = '';
|
|
|
|
|
|
imagePreviewLoading.value = false;
|
|
|
|
|
|
imagePreviewError.value = '';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const onImageLoad = () => {
|
|
|
|
|
|
console.log('✅ 模态框图片加载成功');
|
|
|
|
|
|
// 图片已经在预加载阶段处理了状态,这里只是确认
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const onImageError = (event) => {
|
|
|
|
|
|
console.error('❌ 模态框图片加载失败:', event);
|
|
|
|
|
|
console.error('❌ 失败的图片URL:', imagePreviewUrl.value);
|
|
|
|
|
|
// 这种情况应该很少发生,因为预加载已经验证了URL
|
|
|
|
|
|
imagePreviewLoading.value = false;
|
|
|
|
|
|
imagePreviewError.value = `图片显示失败: ${imagePreviewUrl.value}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const retryLoadImage = () => {
|
|
|
|
|
|
console.log('🔄 重试加载图片');
|
|
|
|
|
|
imagePreviewLoading.value = true;
|
|
|
|
|
|
imagePreviewError.value = '';
|
|
|
|
|
|
startImageLoadTimeout();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-11 04:51:05 +00:00
|
|
|
|
|
2025-10-10 18:39:23 +00:00
|
|
|
|
// 图片加载超时处理
|
|
|
|
|
|
let imageLoadTimeout = null;
|
|
|
|
|
|
const startImageLoadTimeout = () => {
|
|
|
|
|
|
if (imageLoadTimeout) {
|
|
|
|
|
|
clearTimeout(imageLoadTimeout);
|
|
|
|
|
|
}
|
|
|
|
|
|
imageLoadTimeout = setTimeout(() => {
|
|
|
|
|
|
if (imagePreviewLoading.value) {
|
|
|
|
|
|
console.warn('⚠️ 图片加载超时');
|
|
|
|
|
|
imagePreviewLoading.value = false;
|
|
|
|
|
|
imagePreviewError.value = '图片加载超时,请检查网络连接或图片URL是否正确';
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 10000); // 10秒超时
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const clearImageLoadTimeout = () => {
|
|
|
|
|
|
if (imageLoadTimeout) {
|
|
|
|
|
|
clearTimeout(imageLoadTimeout);
|
|
|
|
|
|
imageLoadTimeout = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-04 05:47:42 +00:00
|
|
|
|
// 加载已有思维导图
|
|
|
|
|
|
const loadExistingMindmap = async () => {
|
|
|
|
|
|
// 这里可以实现一个思维导图列表选择器
|
|
|
|
|
|
// 暂时先创建一个新的思维导图
|
|
|
|
|
|
await createNewMindmap();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 创建新思维导图
|
|
|
|
|
|
const createNewMindmap = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await mindmapAPI.createMindmap("新思维导图");
|
|
|
|
|
|
|
|
|
|
|
|
if (response.data && response.data.id) {
|
|
|
|
|
|
const mindmapId = response.data.id;
|
|
|
|
|
|
currentMindmapId.value = mindmapId; // 设置当前思维导图ID
|
|
|
|
|
|
|
|
|
|
|
|
// 创建根节点
|
|
|
|
|
|
const rootNodeResponse = await mindmapAPI.addNodes(mindmapId, {
|
|
|
|
|
|
title: "根节点",
|
|
|
|
|
|
des: "思维导图的起点",
|
|
|
|
|
|
parentId: null
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (rootNodeResponse.data && rootNodeResponse.data.success) {
|
|
|
|
|
|
// 隐藏欢迎页面
|
|
|
|
|
|
hideWelcomePage();
|
|
|
|
|
|
|
|
|
|
|
|
// 加载新创建的思维导图
|
|
|
|
|
|
await loadMindmapData(response.data);
|
|
|
|
|
|
|
|
|
|
|
|
// 显示成功通知
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 延迟后重新加载思维导图数据
|
|
|
|
|
|
setTimeout(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const refreshResponse = await mindmapAPI.getMindmap(mindmapId);
|
|
|
|
|
|
if (refreshResponse.data && refreshResponse.data.nodeData) {
|
|
|
|
|
|
await loadMindmapData(refreshResponse.data);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
// 静默处理错误
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 1500);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
// 静默处理错误
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 保存当前思维导图位置和缩放
|
|
|
|
|
|
const saveCurrentPosition = () => {
|
|
|
|
|
|
if (!mindElixir.value || !mindmapEl.value) return null;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// 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 };
|
2025-09-04 05:47:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn('保存位置失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 恢复思维导图位置和缩放
|
|
|
|
|
|
const restorePosition = (position) => {
|
2025-10-09 06:20:51 +00:00
|
|
|
|
if (!position || !mindmapEl.value || !mindElixir.value) return;
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
try {
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// 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 });
|
2025-09-04 05:47:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn('恢复位置失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 加载思维导图数据 - 支持位置保持
|
2025-10-09 06:20:51 +00:00
|
|
|
|
const loadMindmapData = async (data, keepPosition = false, shouldCenterRoot = true) => {
|
2025-09-04 05:47:42 +00:00
|
|
|
|
try {
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// 只有在明确需要保持位置时才保存当前位置
|
|
|
|
|
|
// 对于添加节点的情况(keepPosition=false, shouldCenterRoot=false),不保存位置,让视图自然居中到新节点
|
2025-09-04 05:47:42 +00:00
|
|
|
|
const currentPosition = keepPosition ? saveCurrentPosition() : null;
|
2025-10-09 06:20:51 +00:00
|
|
|
|
if (currentPosition) {
|
|
|
|
|
|
console.log('📍 保存当前位置用于保持原位:', currentPosition);
|
|
|
|
|
|
} else if (!keepPosition && !shouldCenterRoot) {
|
|
|
|
|
|
console.log('📍 添加节点模式:不保存位置,等待居中到新节点');
|
|
|
|
|
|
}
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
// 设置当前思维导图ID
|
|
|
|
|
|
console.log("🔍 loadMindmapData 被调用,数据:", data);
|
|
|
|
|
|
console.log("🔍 数据字段:", Object.keys(data || {}));
|
|
|
|
|
|
|
|
|
|
|
|
if (data && data.id) {
|
|
|
|
|
|
currentMindmapId.value = data.id;
|
|
|
|
|
|
console.log("🔍 设置当前思维导图ID (data.id):", data.id);
|
|
|
|
|
|
} else if (data && data.mindmapId) {
|
|
|
|
|
|
currentMindmapId.value = data.mindmapId;
|
|
|
|
|
|
console.log("🔍 设置当前思维导图ID (data.mindmapId):", data.mindmapId);
|
|
|
|
|
|
} else if (data && data.nodeData && data.nodeData.mindmapId) {
|
|
|
|
|
|
// 从nodeData中提取思维导图ID
|
|
|
|
|
|
currentMindmapId.value = data.nodeData.mindmapId;
|
|
|
|
|
|
console.log("🔍 设置当前思维导图ID (data.nodeData.mindmapId):", data.nodeData.mindmapId);
|
|
|
|
|
|
} else if (data && data.nodeData && data.nodeData.mindmap_id) {
|
|
|
|
|
|
// 从nodeData中提取思维导图ID(备用字段名)
|
|
|
|
|
|
currentMindmapId.value = data.nodeData.mindmap_id;
|
|
|
|
|
|
console.log("🔍 设置当前思维导图ID (data.nodeData.mindmap_id):", data.nodeData.mindmap_id);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.warn("⚠️ 数据中没有找到 id 或 mindmapId 字段");
|
|
|
|
|
|
console.log("🔍 可用的字段:", Object.keys(data || {}));
|
|
|
|
|
|
if (data && data.nodeData) {
|
|
|
|
|
|
console.log("🔍 nodeData字段:", Object.keys(data.nodeData || {}));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log("🔍 设置后的 currentMindmapId.value:", currentMindmapId.value);
|
|
|
|
|
|
|
|
|
|
|
|
// 隐藏欢迎页面
|
|
|
|
|
|
hideWelcomePage();
|
|
|
|
|
|
|
|
|
|
|
|
// 等待DOM更新完成
|
|
|
|
|
|
await nextTick();
|
|
|
|
|
|
|
|
|
|
|
|
// 确保思维导图容器已经准备好
|
|
|
|
|
|
if (!mindmapEl.value) {
|
|
|
|
|
|
console.warn("⚠️ 思维导图容器未准备好,等待DOM更新...");
|
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
|
|
|
|
|
|
|
|
|
|
if (!mindmapEl.value) {
|
|
|
|
|
|
console.warn("⚠️ 思维导图容器仍未准备好,尝试继续执行...");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建Mind Elixir实例
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (mindmapEl.value) {
|
2025-09-10 07:27:37 +00:00
|
|
|
|
console.log('🔍 创建Mind Elixir实例,设置markdown函数');
|
2025-10-10 10:16:25 +00:00
|
|
|
|
|
2025-09-04 05:47:42 +00:00
|
|
|
|
mindElixir.value = new MindElixir({
|
|
|
|
|
|
el: mindmapEl.value,
|
|
|
|
|
|
direction: MindElixir.RIGHT,
|
|
|
|
|
|
draggable: true,
|
2025-10-09 06:20:51 +00:00
|
|
|
|
contextMenu: false,
|
2025-09-04 05:47:42 +00:00
|
|
|
|
toolBar: true,
|
|
|
|
|
|
nodeMenu: false,
|
2025-10-09 06:20:51 +00:00
|
|
|
|
keypress: false, // 禁用键盘快捷键,防止不入库的添加节点操作
|
2025-09-04 05:47:42 +00:00
|
|
|
|
autoCenter: false,
|
|
|
|
|
|
infinite: true,
|
|
|
|
|
|
maxScale: 5,
|
2025-09-10 07:27:37 +00:00
|
|
|
|
minScale: 0.1,
|
2025-10-10 10:16:25 +00:00
|
|
|
|
theme: customTheme, // 应用自定义主题
|
2025-10-11 04:51:05 +00:00
|
|
|
|
enableNodeBorder: true, // 启用节点边框功能
|
2025-09-10 07:27:37 +00:00
|
|
|
|
markdown: (text, nodeObj) => {
|
2025-10-09 08:02:23 +00:00
|
|
|
|
// 检查内容是否包含markdown语法(包括图片)
|
|
|
|
|
|
if (text.includes('|') || text.includes('**') || text.includes('`') || text.includes('#') || text.includes('![')) {
|
2025-09-10 07:27:37 +00:00
|
|
|
|
const result = smartRenderNodeContent(text);
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
return text;
|
2025-10-10 05:04:03 +00:00
|
|
|
|
},
|
|
|
|
|
|
imageProxy: (url) => {
|
|
|
|
|
|
// 处理图片URL,确保导出时能正确显示
|
|
|
|
|
|
// 如果是相对路径,转换为绝对路径
|
|
|
|
|
|
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
|
|
|
|
return url;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 如果是相对路径,添加当前域名
|
|
|
|
|
|
if (url.startsWith('/')) {
|
|
|
|
|
|
return window.location.origin + url;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 如果是相对路径,添加当前路径
|
|
|
|
|
|
return window.location.origin + '/' + url;
|
2025-09-10 07:27:37 +00:00
|
|
|
|
}
|
2025-09-04 05:47:42 +00:00
|
|
|
|
});
|
2025-09-10 07:27:37 +00:00
|
|
|
|
console.log('✅ Mind Elixir实例创建完成,markdown函数已设置');
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
2025-10-10 05:04:03 +00:00
|
|
|
|
// 注意:导出按钮已由MindElixir原生工具栏提供,无需手动添加
|
|
|
|
|
|
|
2025-09-04 05:47:42 +00:00
|
|
|
|
// 初始化数据
|
2025-09-10 07:27:37 +00:00
|
|
|
|
console.log('🔍 初始化Mind Elixir数据:', data);
|
2025-09-04 05:47:42 +00:00
|
|
|
|
const result = mindElixir.value.init(data);
|
2025-09-10 07:27:37 +00:00
|
|
|
|
console.log('✅ Mind Elixir实例创建成功,初始化结果:', result);
|
|
|
|
|
|
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// 如果有保存的位置,立即恢复位置(因为 init 会自动居中)
|
|
|
|
|
|
if (currentPosition) {
|
|
|
|
|
|
// 立即恢复位置,不延迟
|
|
|
|
|
|
restorePosition(currentPosition);
|
|
|
|
|
|
console.log('📍 初始化后立即恢复位置');
|
|
|
|
|
|
} else if (!keepPosition && !shouldCenterRoot) {
|
|
|
|
|
|
// 对于添加节点的情况,不居中根节点,等待后续居中新节点
|
|
|
|
|
|
console.log('📍 跳过根节点居中,等待居中新节点');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-10 17:47:27 +00:00
|
|
|
|
// 修复初次渲染错位问题:等待DOM稳定后再刷新布局
|
|
|
|
|
|
// 采用多层保障:nextTick + 字体加载 + requestAnimationFrame
|
|
|
|
|
|
const fixInitialRendering = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 第一层:等待Vue DOM更新完成
|
|
|
|
|
|
await nextTick();
|
|
|
|
|
|
|
|
|
|
|
|
// 第二层:等待字体加载完成(关键!)
|
|
|
|
|
|
await document.fonts.ready;
|
|
|
|
|
|
|
|
|
|
|
|
// 第三层:等待下一帧,确保所有渲染管线完成
|
|
|
|
|
|
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
|
|
|
|
|
|
|
|
|
|
if (mindElixir.value && mindElixir.value.refresh) {
|
|
|
|
|
|
console.log('🔄 执行完整DOM稳定后的延迟刷新');
|
|
|
|
|
|
mindElixir.value.refresh();
|
|
|
|
|
|
|
|
|
|
|
|
// 如果设置了居中,在刷新后重新居中
|
|
|
|
|
|
if (!currentPosition && (keepPosition || shouldCenterRoot)) {
|
|
|
|
|
|
mindElixir.value.toCenter();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn('⚠️ 延迟刷新过程中出现错误:', error);
|
|
|
|
|
|
// 降级方案:简单的setTimeout
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (mindElixir.value && mindElixir.value.refresh) {
|
|
|
|
|
|
mindElixir.value.refresh();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 200);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
fixInitialRendering();
|
|
|
|
|
|
|
2025-10-10 18:39:23 +00:00
|
|
|
|
// 添加图片预览事件监听器
|
|
|
|
|
|
mindElixir.value.bus.addListener('showImagePreview', (imageUrl, altText) => {
|
|
|
|
|
|
console.log('🖼️ 收到图片预览事件:', { imageUrl, altText });
|
|
|
|
|
|
openImagePreview(imageUrl, altText);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// Mind Elixir现在会自动使用markdown解析器渲染内容
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.warn("⚠️ 容器未准备好,延迟创建Mind Elixir实例...");
|
|
|
|
|
|
// 延迟创建实例
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (mindmapEl.value) {
|
|
|
|
|
|
mindElixir.value = new MindElixir({
|
|
|
|
|
|
el: mindmapEl.value,
|
|
|
|
|
|
direction: MindElixir.RIGHT,
|
|
|
|
|
|
draggable: true,
|
2025-10-09 06:20:51 +00:00
|
|
|
|
contextMenu: false,
|
2025-09-04 05:47:42 +00:00
|
|
|
|
toolBar: true,
|
|
|
|
|
|
nodeMenu: false,
|
2025-10-09 06:20:51 +00:00
|
|
|
|
keypress: false, // 禁用键盘快捷键,防止不入库的添加节点操作
|
2025-09-04 05:47:42 +00:00
|
|
|
|
autoCenter: false,
|
|
|
|
|
|
infinite: true,
|
|
|
|
|
|
maxScale: 5,
|
2025-09-10 07:27:37 +00:00
|
|
|
|
minScale: 0.1,
|
2025-10-10 10:16:25 +00:00
|
|
|
|
theme: customTheme, // 应用自定义主题
|
2025-10-11 04:51:05 +00:00
|
|
|
|
enableNodeBorder: true, // 启用节点边框功能
|
2025-09-10 07:27:37 +00:00
|
|
|
|
markdown: (text, nodeObj) => {
|
2025-10-09 08:02:23 +00:00
|
|
|
|
// 检查内容是否包含markdown语法(包括图片)
|
|
|
|
|
|
if (text.includes('|') || text.includes('**') || text.includes('`') || text.includes('#') || text.includes('![')) {
|
2025-09-10 07:27:37 +00:00
|
|
|
|
return smartRenderNodeContent(text);
|
|
|
|
|
|
}
|
|
|
|
|
|
return text;
|
|
|
|
|
|
}
|
2025-09-04 05:47:42 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const result = mindElixir.value.init(data);
|
|
|
|
|
|
console.log('✅ Mind Elixir实例延迟创建成功');
|
|
|
|
|
|
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// 如果有保存的位置,立即恢复位置(因为 init 会自动居中)
|
|
|
|
|
|
if (currentPosition) {
|
|
|
|
|
|
restorePosition(currentPosition);
|
|
|
|
|
|
console.log('📍 延迟创建后立即恢复位置');
|
|
|
|
|
|
|
|
|
|
|
|
// 再次确保位置恢复
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
restorePosition(currentPosition);
|
|
|
|
|
|
console.log('📍 延迟创建后二次确认位置恢复');
|
|
|
|
|
|
}, 100);
|
|
|
|
|
|
} else if (!keepPosition && !shouldCenterRoot) {
|
|
|
|
|
|
// 对于添加节点的情况,不居中根节点,等待后续居中新节点
|
|
|
|
|
|
console.log('📍 延迟创建后跳过根节点居中,等待居中新节点');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-10 17:47:27 +00:00
|
|
|
|
// 修复初次渲染错位问题:延迟创建实例的完整DOM稳定保障
|
|
|
|
|
|
const fixDelayedRendering = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 第一层:等待Vue DOM更新完成
|
|
|
|
|
|
await nextTick();
|
|
|
|
|
|
|
|
|
|
|
|
// 第二层:等待字体加载完成
|
|
|
|
|
|
await document.fonts.ready;
|
|
|
|
|
|
|
|
|
|
|
|
// 第三层:等待下一帧,确保所有渲染管线完成
|
|
|
|
|
|
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
|
|
|
|
|
|
|
|
|
|
if (mindElixir.value && mindElixir.value.refresh) {
|
|
|
|
|
|
console.log('🔄 延迟创建实例执行完整DOM稳定后的延迟刷新');
|
|
|
|
|
|
mindElixir.value.refresh();
|
|
|
|
|
|
|
|
|
|
|
|
// 如果设置了居中,在刷新后重新居中
|
|
|
|
|
|
if (!currentPosition && (keepPosition || shouldCenterRoot)) {
|
|
|
|
|
|
mindElixir.value.toCenter();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn('⚠️ 延迟创建实例的延迟刷新过程中出现错误:', error);
|
|
|
|
|
|
// 降级方案:简单的setTimeout
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (mindElixir.value && mindElixir.value.refresh) {
|
|
|
|
|
|
mindElixir.value.refresh();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 250);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
fixDelayedRendering();
|
|
|
|
|
|
|
2025-10-10 18:39:23 +00:00
|
|
|
|
// 添加图片预览事件监听器
|
|
|
|
|
|
mindElixir.value.bus.addListener('showImagePreview', (imageUrl, altText) => {
|
|
|
|
|
|
console.log('🖼️ 收到图片预览事件(延迟创建):', { imageUrl, altText });
|
|
|
|
|
|
openImagePreview(imageUrl, altText);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-04 05:47:42 +00:00
|
|
|
|
// 延迟执行后续操作
|
|
|
|
|
|
setTimeout(() => {
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// Mind Elixir现在会自动使用markdown解析器渲染内容
|
|
|
|
|
|
|
2025-10-09 06:20:51 +00:00
|
|
|
|
if (currentPosition) {
|
2025-09-04 05:47:42 +00:00
|
|
|
|
restorePosition(currentPosition);
|
2025-10-09 06:20:51 +00:00
|
|
|
|
} else if (shouldCenterRoot) {
|
2025-09-04 05:47:42 +00:00
|
|
|
|
centerMindMap();
|
|
|
|
|
|
}
|
|
|
|
|
|
bindEventListeners();
|
2025-09-08 07:06:08 +00:00
|
|
|
|
// 添加节点描述显示
|
|
|
|
|
|
addNodeDescriptions();
|
2025-09-04 05:47:42 +00:00
|
|
|
|
}, 300);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 200);
|
|
|
|
|
|
return; // 提前返回,避免执行后续代码
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.warn("⚠️ Mind Elixir实例创建失败,尝试延迟创建:", error);
|
2025-09-04 05:47:42 +00:00
|
|
|
|
// 延迟重试
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (mindmapEl.value) {
|
|
|
|
|
|
mindElixir.value = new MindElixir({
|
|
|
|
|
|
el: mindmapEl.value,
|
|
|
|
|
|
direction: MindElixir.RIGHT,
|
|
|
|
|
|
draggable: true,
|
2025-10-09 06:20:51 +00:00
|
|
|
|
contextMenu: false,
|
2025-09-04 05:47:42 +00:00
|
|
|
|
toolBar: true,
|
|
|
|
|
|
nodeMenu: false,
|
2025-10-09 06:20:51 +00:00
|
|
|
|
keypress: false, // 禁用键盘快捷键,防止不入库的添加节点操作
|
2025-09-04 05:47:42 +00:00
|
|
|
|
autoCenter: false,
|
|
|
|
|
|
infinite: true,
|
|
|
|
|
|
maxScale: 5,
|
2025-09-10 07:27:37 +00:00
|
|
|
|
minScale: 0.1,
|
2025-10-10 10:16:25 +00:00
|
|
|
|
theme: customTheme, // 应用自定义主题
|
2025-10-11 04:51:05 +00:00
|
|
|
|
enableNodeBorder: true, // 启用节点边框功能
|
2025-09-10 07:27:37 +00:00
|
|
|
|
markdown: (text, nodeObj) => {
|
2025-10-09 08:02:23 +00:00
|
|
|
|
// 检查内容是否包含markdown语法(包括图片)
|
|
|
|
|
|
if (text.includes('|') || text.includes('**') || text.includes('`') || text.includes('#') || text.includes('![')) {
|
2025-09-10 07:27:37 +00:00
|
|
|
|
return smartRenderNodeContent(text);
|
|
|
|
|
|
}
|
|
|
|
|
|
return text;
|
|
|
|
|
|
}
|
2025-09-04 05:47:42 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const result = mindElixir.value.init(data);
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log('✅ Mind Elixir实例重试创建成功');
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// 如果有保存的位置,立即恢复位置(因为 init 会自动居中)
|
|
|
|
|
|
if (currentPosition) {
|
|
|
|
|
|
restorePosition(currentPosition);
|
|
|
|
|
|
console.log('📍 重试创建后立即恢复位置');
|
|
|
|
|
|
|
|
|
|
|
|
// 再次确保位置恢复
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
restorePosition(currentPosition);
|
|
|
|
|
|
console.log('📍 重试创建后二次确认位置恢复');
|
|
|
|
|
|
}, 100);
|
|
|
|
|
|
} else if (!keepPosition && !shouldCenterRoot) {
|
|
|
|
|
|
// 对于添加节点的情况,不居中根节点,等待后续居中新节点
|
|
|
|
|
|
console.log('📍 重试创建后跳过根节点居中,等待居中新节点');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-04 05:47:42 +00:00
|
|
|
|
// 延迟执行后续操作
|
|
|
|
|
|
setTimeout(() => {
|
2025-10-09 06:20:51 +00:00
|
|
|
|
if (currentPosition) {
|
2025-09-04 05:47:42 +00:00
|
|
|
|
restorePosition(currentPosition);
|
2025-10-09 06:20:51 +00:00
|
|
|
|
} else if (shouldCenterRoot) {
|
2025-09-04 05:47:42 +00:00
|
|
|
|
centerMindMap();
|
|
|
|
|
|
}
|
|
|
|
|
|
bindEventListeners();
|
2025-09-08 07:06:08 +00:00
|
|
|
|
// 添加节点描述显示
|
|
|
|
|
|
addNodeDescriptions();
|
2025-09-04 05:47:42 +00:00
|
|
|
|
}, 300);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (retryError) {
|
|
|
|
|
|
console.error("❌ Mind Elixir实例重试创建失败:", retryError);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 500);
|
|
|
|
|
|
return; // 提前返回,避免执行后续代码
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理位置恢复或居中
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (keepPosition && currentPosition) {
|
|
|
|
|
|
restorePosition(currentPosition);
|
2025-10-09 06:20:51 +00:00
|
|
|
|
} else if (shouldCenterRoot) {
|
2025-09-04 05:47:42 +00:00
|
|
|
|
centerMindMap();
|
|
|
|
|
|
}
|
|
|
|
|
|
hideWelcomePage();
|
|
|
|
|
|
bindEventListeners();
|
|
|
|
|
|
|
2025-10-10 05:04:03 +00:00
|
|
|
|
// 延迟添加节点描述显示
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
addNodeDescriptions();
|
|
|
|
|
|
}, 500);
|
2025-09-04 05:47:42 +00:00
|
|
|
|
}, 100);
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 转换数据格式
|
|
|
|
|
|
const convertToMindElixirFormat = (data) => {
|
|
|
|
|
|
const mindmap = data.mindmap || data;
|
|
|
|
|
|
const nodes = data.nodes || [];
|
|
|
|
|
|
const mindmapId = data.id || mindmap.id;
|
|
|
|
|
|
|
|
|
|
|
|
// console.log("原始节点数据:", nodes);
|
|
|
|
|
|
// console.log("思维导图ID:", mindmapId);
|
|
|
|
|
|
|
|
|
|
|
|
// 过滤掉已删除的节点
|
|
|
|
|
|
const activeNodes = nodes.filter(node => !node.delete);
|
|
|
|
|
|
// console.log("过滤后的活跃节点:", activeNodes);
|
|
|
|
|
|
|
|
|
|
|
|
// 构建节点映射
|
|
|
|
|
|
const nodeMap = new Map();
|
|
|
|
|
|
activeNodes.forEach(node => {
|
|
|
|
|
|
nodeMap.set(node.id, {
|
|
|
|
|
|
id: node.id,
|
|
|
|
|
|
topic: node.title || node.content || "无标题",
|
|
|
|
|
|
data: {
|
2025-09-08 07:06:08 +00:00
|
|
|
|
des: node.des || ""
|
2025-09-04 05:47:42 +00:00
|
|
|
|
},
|
|
|
|
|
|
children: [],
|
|
|
|
|
|
// 添加思维导图ID到节点
|
|
|
|
|
|
mindmapId: mindmapId,
|
|
|
|
|
|
mindmap_id: mindmapId
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 构建树形结构
|
|
|
|
|
|
const rootNodes = [];
|
|
|
|
|
|
activeNodes.forEach(node => {
|
|
|
|
|
|
const mindElixirNode = nodeMap.get(node.id);
|
|
|
|
|
|
if (node.parentId && nodeMap.has(node.parentId)) {
|
|
|
|
|
|
const parent = nodeMap.get(node.parentId);
|
|
|
|
|
|
parent.children.push(mindElixirNode);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
rootNodes.push(mindElixirNode);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// console.log("构建的树形结构:", rootNodes);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
nodeData: rootNodes[0] || {
|
|
|
|
|
|
id: "root",
|
|
|
|
|
|
topic: "根节点",
|
|
|
|
|
|
data: { des: "思维导图的起点" },
|
|
|
|
|
|
children: [],
|
|
|
|
|
|
mindmapId: mindmapId,
|
|
|
|
|
|
mindmap_id: mindmapId
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 为节点添加描述显示
|
|
|
|
|
|
const addNodeDescriptions = () => {
|
|
|
|
|
|
if (!mindElixir.value) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!mindmapEl.value) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// console.log('开始添加节点描述...');
|
|
|
|
|
|
// console.log('MindElixir数据:', mindElixir.value.data);
|
|
|
|
|
|
|
|
|
|
|
|
// 等待一下让MindElixir完全渲染
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
// 获取所有节点元素 - 尝试多种选择器
|
|
|
|
|
|
const nodeElements = mindmapEl.value.querySelectorAll('me-tpc, .topic, [data-id]');
|
|
|
|
|
|
// console.log('找到节点元素数量:', nodeElements.length);
|
|
|
|
|
|
|
|
|
|
|
|
nodeElements.forEach((nodeElement, index) => {
|
|
|
|
|
|
// console.log(`处理节点 ${index}:`, nodeElement);
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试多种方式获取节点ID
|
|
|
|
|
|
const nodeId = nodeElement.getAttribute('data-id') ||
|
|
|
|
|
|
nodeElement.id ||
|
|
|
|
|
|
nodeElement.getAttribute('nodeid') ||
|
|
|
|
|
|
nodeElement.querySelector('[data-id]')?.getAttribute('data-id');
|
|
|
|
|
|
|
|
|
|
|
|
// console.log('节点ID:', nodeId);
|
|
|
|
|
|
if (!nodeId) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 查找对应的节点数据
|
|
|
|
|
|
const findNodeData = (nodes, id) => {
|
|
|
|
|
|
for (const node of nodes) {
|
|
|
|
|
|
if (node.id === id) return node;
|
|
|
|
|
|
if (node.children) {
|
|
|
|
|
|
const found = findNodeData(node.children, id);
|
|
|
|
|
|
if (found) return found;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const nodeData = findNodeData(mindElixir.value.data.nodeData, nodeId);
|
|
|
|
|
|
// console.log('节点数据:', nodeData);
|
|
|
|
|
|
|
|
|
|
|
|
if (nodeData && nodeData.data && nodeData.data.des) {
|
|
|
|
|
|
// console.log('找到描述内容:', nodeData.data.des.substring(0, 50) + '...');
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否已经有描述元素
|
|
|
|
|
|
let descElement = nodeElement.querySelector('.node-description');
|
|
|
|
|
|
if (!descElement) {
|
|
|
|
|
|
descElement = document.createElement('div');
|
|
|
|
|
|
descElement.className = 'node-description';
|
|
|
|
|
|
descElement.style.cssText = `
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
margin-top: 6px;
|
|
|
|
|
|
padding: 6px 8px;
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.03);
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
max-width: 250px;
|
|
|
|
|
|
word-wrap: break-word;
|
|
|
|
|
|
line-height: 1.3;
|
|
|
|
|
|
border-left: 3px solid #e0e0e0;
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// 将描述元素添加到节点中
|
|
|
|
|
|
nodeElement.appendChild(descElement);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 设置描述内容
|
|
|
|
|
|
const description = nodeData.data.des;
|
|
|
|
|
|
if (description.length > 150) {
|
|
|
|
|
|
descElement.textContent = description.substring(0, 150) + '...';
|
|
|
|
|
|
descElement.title = description;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
descElement.textContent = description;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// console.log('已设置描述内容');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}, 1000); // 等待1秒让MindElixir完全渲染
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 移除强制样式,恢复MindElixir原始样式
|
|
|
|
|
|
const removeForcedStyles = () => {
|
|
|
|
|
|
if (!mindmapEl.value) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 延迟执行,确保MindElixir完全渲染
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
// 获取所有节点元素
|
|
|
|
|
|
const allNodes = mindmapEl.value.querySelectorAll('me-tpc, .topic, [data-id]');
|
|
|
|
|
|
|
|
|
|
|
|
allNodes.forEach((node) => {
|
|
|
|
|
|
// 移除所有强制样式,恢复MindElixir原始样式
|
|
|
|
|
|
node.removeAttribute('style');
|
|
|
|
|
|
|
|
|
|
|
|
// 移除文本元素的强制样式
|
|
|
|
|
|
const textElement = node.querySelector('.topic-text');
|
|
|
|
|
|
if (textElement) {
|
|
|
|
|
|
textElement.removeAttribute('style');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// console.log('已移除强制样式,恢复MindElixir原始样式');
|
|
|
|
|
|
}, 100);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 移除连接线修复,让MindElixir使用原始连接线
|
|
|
|
|
|
const removeConnectionLineFixes = () => {
|
|
|
|
|
|
if (!mindmapEl.value) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 移除所有对连接线的强制样式修改
|
|
|
|
|
|
const lines = mindmapEl.value.querySelectorAll('svg line, svg path');
|
|
|
|
|
|
|
|
|
|
|
|
lines.forEach((line) => {
|
|
|
|
|
|
// 移除强制样式,恢复MindElixir原始样式
|
|
|
|
|
|
line.removeAttribute('style');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// console.log('已移除连接线强制样式');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 节点拖拽处理函数
|
|
|
|
|
|
const handleNodeDragStart = (event, nodeId) => {
|
|
|
|
|
|
isDragging.value = true;
|
|
|
|
|
|
currentNodeId.value = nodeId;
|
|
|
|
|
|
|
|
|
|
|
|
// 记录拖拽开始位置
|
|
|
|
|
|
const rect = event.target.getBoundingClientRect();
|
|
|
|
|
|
dragStartPos.value = {
|
|
|
|
|
|
x: event.clientX - rect.left,
|
|
|
|
|
|
y: event.clientY - rect.top
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleNodeDrag = (event) => {
|
|
|
|
|
|
if (!isDragging.value || !currentNodeId.value) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 计算偏移量
|
|
|
|
|
|
const offsetX = event.clientX - dragStartPos.value.x;
|
|
|
|
|
|
const offsetY = event.clientY - dragStartPos.value.y;
|
|
|
|
|
|
|
|
|
|
|
|
// 更新节点位置
|
|
|
|
|
|
const nodeElement = mindmapEl.value.querySelector(`[data-id="${currentNodeId.value}"]`);
|
|
|
|
|
|
if (nodeElement) {
|
|
|
|
|
|
// 禁用过渡动画,避免跳变
|
|
|
|
|
|
nodeElement.style.transition = 'none';
|
|
|
|
|
|
nodeElement.style.transform = `translate(${offsetX}px, ${offsetY}px)`;
|
|
|
|
|
|
|
|
|
|
|
|
// 保存位置偏移
|
|
|
|
|
|
nodePositions.value.set(currentNodeId.value, { x: offsetX, y: offsetY });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新连线
|
|
|
|
|
|
updateConnectionLines();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleNodeDragEnd = () => {
|
|
|
|
|
|
if (!isDragging.value || !currentNodeId.value) return;
|
|
|
|
|
|
|
|
|
|
|
|
isDragging.value = false;
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log('🎯 结束拖拽节点:', currentNodeId.value);
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
// 使用防抖保存,避免频繁保存
|
|
|
|
|
|
debouncedSave(currentNodeId.value);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 更新连线位置
|
|
|
|
|
|
const updateConnectionLines = () => {
|
|
|
|
|
|
if (!mindmapEl.value) return;
|
|
|
|
|
|
|
|
|
|
|
|
const lines = mindmapEl.value.querySelectorAll('svg line, svg path');
|
|
|
|
|
|
|
|
|
|
|
|
lines.forEach((line) => {
|
|
|
|
|
|
// 获取连接线的起点和终点节点
|
|
|
|
|
|
const startNodeId = line.getAttribute('data-start');
|
|
|
|
|
|
const endNodeId = line.getAttribute('data-end');
|
|
|
|
|
|
|
|
|
|
|
|
if (startNodeId && endNodeId) {
|
|
|
|
|
|
const startNode = mindmapEl.value.querySelector(`[data-id="${startNodeId}"]`);
|
|
|
|
|
|
const endNode = mindmapEl.value.querySelector(`[data-id="${endNodeId}"]`);
|
|
|
|
|
|
|
|
|
|
|
|
if (startNode && endNode) {
|
|
|
|
|
|
const startRect = startNode.getBoundingClientRect();
|
|
|
|
|
|
const endRect = endNode.getBoundingClientRect();
|
|
|
|
|
|
const containerRect = mindmapEl.value.getBoundingClientRect();
|
|
|
|
|
|
|
|
|
|
|
|
// 计算连线的新位置
|
|
|
|
|
|
const startX = startRect.left + startRect.width / 2 - containerRect.left;
|
|
|
|
|
|
const startY = startRect.top + startRect.height / 2 - containerRect.top;
|
|
|
|
|
|
const endX = endRect.left + endRect.width / 2 - containerRect.left;
|
|
|
|
|
|
const endY = endRect.top + endRect.height / 2 - containerRect.top;
|
|
|
|
|
|
|
|
|
|
|
|
// 更新连线
|
|
|
|
|
|
if (line.tagName === 'line') {
|
|
|
|
|
|
line.setAttribute('x1', startX);
|
|
|
|
|
|
line.setAttribute('y1', startY);
|
|
|
|
|
|
line.setAttribute('x2', endX);
|
|
|
|
|
|
line.setAttribute('y2', endY);
|
|
|
|
|
|
} else if (line.tagName === 'path') {
|
|
|
|
|
|
const pathData = `M ${startX} ${startY} L ${endX} ${endY}`;
|
|
|
|
|
|
line.setAttribute('d', pathData);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 保存节点位置和结构到后端
|
|
|
|
|
|
const saveNodePosition = async (nodeId) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (!currentMindmapId.value) {
|
|
|
|
|
|
console.warn('⚠️ 当前思维导图ID不存在,无法保存节点位置');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取Mind Elixir的当前数据结构
|
|
|
|
|
|
if (!mindElixir.value || !mindElixir.value.data) {
|
|
|
|
|
|
console.warn('⚠️ Mind Elixir数据不存在,无法保存节点位置');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const currentData = mindElixir.value.data;
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log('💾 开始保存拖拽后的节点结构...');
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
// 使用现有的updateNode接口保存节点结构
|
|
|
|
|
|
try {
|
|
|
|
|
|
const draggedNode = findNodeInTree([currentData.nodeData], nodeId);
|
|
|
|
|
|
if (draggedNode && currentMindmapId.value) {
|
|
|
|
|
|
// 保存拖拽后的节点数据
|
|
|
|
|
|
const response = await mindmapAPI.updateNode(nodeId, {
|
|
|
|
|
|
...draggedNode,
|
|
|
|
|
|
mindmapId: currentMindmapId.value,
|
|
|
|
|
|
lastModified: Date.now()
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (response.data && response.data.success) {
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log('✅ 拖拽后的节点保存成功');
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.warn('⚠️ 保存节点失败:', response);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('❌ 保存节点到后端失败:', error);
|
|
|
|
|
|
// 尝试使用备用方法:更新单个节点
|
|
|
|
|
|
await saveSingleNodePosition(nodeId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('❌ 保存节点位置失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 防抖保存函数
|
|
|
|
|
|
let saveTimeout = null;
|
|
|
|
|
|
const debouncedSave = (nodeId) => {
|
|
|
|
|
|
if (saveTimeout) {
|
|
|
|
|
|
clearTimeout(saveTimeout);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
saveTimeout = setTimeout(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await saveNodePosition(nodeId);
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log('✅ 防抖保存完成');
|
2025-09-04 05:47:42 +00:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('❌ 防抖保存失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 1000); // 1秒防抖
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 备用方法:保存单个节点位置
|
|
|
|
|
|
const saveSingleNodePosition = async (nodeId) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (!mindElixir.value || !mindElixir.value.data) return;
|
|
|
|
|
|
|
|
|
|
|
|
const nodeData = findNodeInTree([mindElixir.value.data.nodeData], nodeId);
|
|
|
|
|
|
if (nodeData && currentMindmapId.value) {
|
|
|
|
|
|
// 更新节点数据
|
|
|
|
|
|
await mindmapAPI.updateNode(nodeId, {
|
|
|
|
|
|
...nodeData,
|
|
|
|
|
|
mindmapId: currentMindmapId.value,
|
|
|
|
|
|
lastModified: Date.now()
|
|
|
|
|
|
});
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log('✅ 备用方法:单个节点保存成功');
|
2025-09-04 05:47:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('❌ 备用方法保存失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 通用的节点查找函数
|
|
|
|
|
|
const findNodeInTree = (nodes, targetId) => {
|
|
|
|
|
|
for (const node of nodes) {
|
|
|
|
|
|
if (node.id === targetId) return node;
|
|
|
|
|
|
if (node.children && node.children.length > 0) {
|
|
|
|
|
|
const found = findNodeInTree(node.children, targetId);
|
|
|
|
|
|
if (found) return found;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 应用自定义节点位置
|
|
|
|
|
|
const applyCustomNodePositions = () => {
|
|
|
|
|
|
if (!mindmapEl.value) return;
|
|
|
|
|
|
|
|
|
|
|
|
nodePositions.value.forEach((position, nodeId) => {
|
|
|
|
|
|
const nodeElement = mindmapEl.value.querySelector(`[data-id="${nodeId}"]`);
|
|
|
|
|
|
if (nodeElement) {
|
|
|
|
|
|
// 应用保存的位置偏移
|
|
|
|
|
|
nodeElement.style.transform = `translate(${position.x}px, ${position.y}px)`;
|
|
|
|
|
|
nodeElement.style.transition = 'none'; // 禁用过渡动画
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 更新连线
|
|
|
|
|
|
updateConnectionLines();
|
|
|
|
|
|
|
|
|
|
|
|
// console.log('已应用自定义节点位置');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 计算思维导图整体位置并居中显示
|
|
|
|
|
|
const centerMindMap = () => {
|
|
|
|
|
|
try {
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// 使用 MindElixir 自带的 toCenter 方法实现根节点居中
|
|
|
|
|
|
if (mindElixir.value && mindElixir.value.toCenter) {
|
|
|
|
|
|
mindElixir.value.toCenter();
|
|
|
|
|
|
console.log('✅ 使用 MindElixir toCenter 方法实现根节点居中');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果 toCenter 方法不可用,使用原有的手动居中方法作为备用方案
|
2025-09-04 05:47:42 +00:00
|
|
|
|
const mindmapContainer = mindmapEl.value;
|
|
|
|
|
|
if (!mindmapContainer) return;
|
|
|
|
|
|
|
|
|
|
|
|
const canvas = mindmapContainer.querySelector('.map-canvas');
|
|
|
|
|
|
if (!canvas) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 获取所有节点元素
|
|
|
|
|
|
const nodeElements = canvas.querySelectorAll('me-tpc');
|
|
|
|
|
|
if (nodeElements.length === 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
// console.log("找到节点数量:", nodeElements.length);
|
|
|
|
|
|
|
|
|
|
|
|
// 计算所有节点的边界
|
|
|
|
|
|
let minX = Infinity;
|
|
|
|
|
|
let maxX = -Infinity;
|
|
|
|
|
|
let minY = Infinity;
|
|
|
|
|
|
let maxY = -Infinity;
|
|
|
|
|
|
|
|
|
|
|
|
nodeElements.forEach(node => {
|
|
|
|
|
|
const rect = node.getBoundingClientRect();
|
|
|
|
|
|
const containerRect = mindmapContainer.getBoundingClientRect();
|
|
|
|
|
|
|
|
|
|
|
|
// 转换为相对于容器的坐标
|
|
|
|
|
|
const relativeX = rect.left - containerRect.left;
|
|
|
|
|
|
const relativeY = rect.top - containerRect.top;
|
|
|
|
|
|
|
|
|
|
|
|
minX = Math.min(minX, relativeX);
|
|
|
|
|
|
maxX = Math.max(maxX, relativeX + rect.width);
|
|
|
|
|
|
minY = Math.min(minY, relativeY);
|
|
|
|
|
|
maxY = Math.max(maxY, relativeY + rect.height);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// console.log("思维导图边界:", { minX, maxX, minY, maxY });
|
|
|
|
|
|
|
|
|
|
|
|
// 计算思维导图的中心点
|
|
|
|
|
|
const mindMapCenterX = (minX + maxX) / 2;
|
|
|
|
|
|
const mindMapCenterY = (minY + maxY) / 2;
|
|
|
|
|
|
|
|
|
|
|
|
// 计算容器的中心点
|
|
|
|
|
|
const containerCenterX = mindmapContainer.clientWidth / 2;
|
|
|
|
|
|
const containerCenterY = mindmapContainer.clientHeight / 2;
|
|
|
|
|
|
|
|
|
|
|
|
// 计算需要移动的距离
|
|
|
|
|
|
const offsetX = containerCenterX - mindMapCenterX;
|
|
|
|
|
|
const offsetY = containerCenterY - mindMapCenterY;
|
|
|
|
|
|
|
|
|
|
|
|
// console.log("居中计算:", {
|
|
|
|
|
|
// mindMapCenter: { x: mindMapCenterX, y: mindMapCenterY },
|
|
|
|
|
|
// containerCenter: { x: containerCenterX, y: containerCenterY },
|
|
|
|
|
|
// offset: { x: offsetX, y: offsetY }
|
|
|
|
|
|
// });
|
|
|
|
|
|
|
|
|
|
|
|
// 应用变换,使思维导图居中
|
|
|
|
|
|
canvas.style.transform = `translate(${offsetX}px, ${offsetY}px)`;
|
|
|
|
|
|
|
|
|
|
|
|
// 显示画布
|
|
|
|
|
|
canvas.style.opacity = '1';
|
|
|
|
|
|
canvas.style.transition = 'opacity 0.3s ease';
|
|
|
|
|
|
|
|
|
|
|
|
// 确保思维导图完全可见,添加足够的边距
|
|
|
|
|
|
const padding = 100; // 增加边距,确保节点不被遮挡
|
|
|
|
|
|
const adjustedOffsetX = Math.max(offsetX, padding);
|
|
|
|
|
|
const adjustedOffsetY = Math.max(offsetY, padding);
|
|
|
|
|
|
|
|
|
|
|
|
// 应用最终的变换
|
|
|
|
|
|
canvas.style.transform = `translate(${adjustedOffsetX}px, ${adjustedOffsetY}px)`;
|
|
|
|
|
|
|
|
|
|
|
|
// 确保画布可见
|
|
|
|
|
|
canvas.style.opacity = '1';
|
|
|
|
|
|
canvas.style.visibility = 'visible';
|
|
|
|
|
|
|
|
|
|
|
|
// 思维导图已居中,添加一些调试信息
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log('🎯 思维导图已居中,边距:', padding, '偏移:', { x: adjustedOffsetX, y: adjustedOffsetY });
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
// 静默处理错误
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// 居中显示指定节点并进入编辑状态(平滑动画版本)
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-04 05:47:42 +00:00
|
|
|
|
// 计算菜单位置
|
|
|
|
|
|
const calculateMenuPosition = () => {
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// console.log("🎯 计算菜单位置,选中节点:", selectedNode.value); // ✅ 注释掉频繁日志
|
2025-09-04 05:47:42 +00:00
|
|
|
|
if (!selectedNode.value) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 使用Mind Elixir的API获取节点位置
|
|
|
|
|
|
if (mindElixir.value && mindElixir.value.getNodeById) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const nodeElement = mindElixir.value.getNodeById(selectedNode.value.id);
|
|
|
|
|
|
// console.log("通过Mind Elixir API获取节点:", nodeElement);
|
|
|
|
|
|
if (nodeElement) {
|
|
|
|
|
|
const rect = nodeElement.getBoundingClientRect();
|
|
|
|
|
|
const containerRect = mindmapEl.value.getBoundingClientRect();
|
|
|
|
|
|
|
|
|
|
|
|
// console.log("节点位置:", rect);
|
|
|
|
|
|
// console.log("容器位置:", containerRect);
|
|
|
|
|
|
|
|
|
|
|
|
// 计算菜单位置 - 显示在节点下方
|
|
|
|
|
|
const left = rect.left - containerRect.left + rect.width / 2;
|
|
|
|
|
|
const top = rect.bottom - containerRect.top + 10;
|
|
|
|
|
|
|
|
|
|
|
|
contextMenuStyle.value = {
|
|
|
|
|
|
left: `${left}px`,
|
|
|
|
|
|
top: `${top}px`
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// console.log("菜单位置:", contextMenuStyle.value);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
// 静默处理错误
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取选中的节点元素 - 尝试多种选择器
|
|
|
|
|
|
let nodeElement = document.querySelector(`[data-id="${selectedNode.value.id}"]`);
|
|
|
|
|
|
if (!nodeElement) {
|
|
|
|
|
|
nodeElement = document.querySelector(`.topic[data-id="${selectedNode.value.id}"]`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!nodeElement) {
|
|
|
|
|
|
nodeElement = document.querySelector(`[data-node-id="${selectedNode.value.id}"]`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!nodeElement) {
|
|
|
|
|
|
// 尝试Mind Elixir的data-nodeid格式(带me前缀)
|
|
|
|
|
|
nodeElement = document.querySelector(`[data-nodeid="me${selectedNode.value.id}"]`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!nodeElement) {
|
|
|
|
|
|
// 尝试查找me-tpc元素
|
|
|
|
|
|
const meTpcElements = document.querySelectorAll('me-tpc');
|
|
|
|
|
|
for (const element of meTpcElements) {
|
|
|
|
|
|
if (element.getAttribute('data-nodeid') === `me${selectedNode.value.id}`) {
|
|
|
|
|
|
nodeElement = element;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!nodeElement) {
|
|
|
|
|
|
// 尝试通过文本内容查找
|
|
|
|
|
|
const topics = document.querySelectorAll('.topic');
|
|
|
|
|
|
for (const topic of topics) {
|
|
|
|
|
|
if (topic.textContent.trim() === selectedNode.value.topic) {
|
|
|
|
|
|
nodeElement = topic;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// ❌ 移除这个性能杀手 - 不要遍历所有元素!
|
|
|
|
|
|
// 如果实在找不到,就使用默认位置
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
// console.log("找到节点元素:", nodeElement);
|
|
|
|
|
|
|
|
|
|
|
|
if (nodeElement) {
|
|
|
|
|
|
const rect = nodeElement.getBoundingClientRect();
|
|
|
|
|
|
const containerRect = mindmapEl.value.getBoundingClientRect();
|
|
|
|
|
|
|
|
|
|
|
|
// console.log("节点位置:", rect);
|
|
|
|
|
|
// console.log("容器位置:", containerRect);
|
|
|
|
|
|
|
|
|
|
|
|
// 计算菜单位置 - 显示在节点下方
|
|
|
|
|
|
const left = rect.left - containerRect.left + rect.width / 2;
|
|
|
|
|
|
const top = rect.bottom - containerRect.top + 10;
|
|
|
|
|
|
|
|
|
|
|
|
contextMenuStyle.value = {
|
|
|
|
|
|
left: `${left}px`,
|
|
|
|
|
|
top: `${top}px`
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// console.log("菜单位置:", contextMenuStyle.value);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// console.log("未找到节点元素,使用默认位置");
|
|
|
|
|
|
// 使用默认位置
|
|
|
|
|
|
contextMenuStyle.value = {
|
|
|
|
|
|
left: "50%",
|
|
|
|
|
|
top: "50%",
|
|
|
|
|
|
transform: "translate(-50%, -50%)"
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 节点操作方法
|
|
|
|
|
|
const addChildNode = async () => {
|
|
|
|
|
|
if (!selectedNode.value) return;
|
|
|
|
|
|
await addChildNodeToAPI(selectedNode.value);
|
|
|
|
|
|
// 操作完成后隐藏菜单
|
|
|
|
|
|
selectedNode.value = null;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const addSiblingNode = async () => {
|
|
|
|
|
|
if (!selectedNode.value) return;
|
|
|
|
|
|
await addSiblingNodeToAPI(selectedNode.value);
|
|
|
|
|
|
// 操作完成后隐藏菜单
|
|
|
|
|
|
selectedNode.value = null;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const deleteSelectedNode = async () => {
|
|
|
|
|
|
if (!selectedNode.value) return;
|
|
|
|
|
|
await deleteNodeFromAPI(selectedNode.value);
|
|
|
|
|
|
// 操作完成后隐藏菜单
|
|
|
|
|
|
selectedNode.value = null;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-08 07:06:08 +00:00
|
|
|
|
// Ask AI 功能
|
|
|
|
|
|
const askAIForNode = async () => {
|
|
|
|
|
|
if (!selectedNode.value) return;
|
|
|
|
|
|
|
|
|
|
|
|
console.log('Ask AI for node:', selectedNode.value);
|
|
|
|
|
|
|
|
|
|
|
|
// 计算AI输入区域位置 - 显示在横向菜单下方
|
|
|
|
|
|
const menuLeft = parseFloat(contextMenuStyle.value.left) || 0;
|
|
|
|
|
|
const menuTop = parseFloat(contextMenuStyle.value.top) || 0;
|
|
|
|
|
|
|
|
|
|
|
|
aiInputStyle.value = {
|
|
|
|
|
|
left: `${menuLeft}px`,
|
|
|
|
|
|
top: `${menuTop + 60}px`, // 在菜单下方60px处
|
|
|
|
|
|
transform: 'translateX(-50%)'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 显示AI输入区域
|
|
|
|
|
|
showAIDialog.value = true;
|
|
|
|
|
|
currentQuestionNode.value = selectedNode.value;
|
|
|
|
|
|
|
|
|
|
|
|
// 不清空selectedNode,保持横向菜单显示
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 关闭AI对话框
|
|
|
|
|
|
const closeAIDialog = () => {
|
|
|
|
|
|
showAIDialog.value = false;
|
|
|
|
|
|
currentQuestionNode.value = null;
|
|
|
|
|
|
aiQuestion.value = '';
|
|
|
|
|
|
isAIProcessing.value = false;
|
|
|
|
|
|
// 关闭AI输入框时也清空选中的节点,隐藏横向菜单
|
|
|
|
|
|
selectedNode.value = null;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取节点上下文信息(包括父节点、祖父节点)
|
|
|
|
|
|
const getNodeContext = (node) => {
|
|
|
|
|
|
if (!node) return '';
|
|
|
|
|
|
|
|
|
|
|
|
const context = [];
|
|
|
|
|
|
|
|
|
|
|
|
// 获取父节点信息
|
|
|
|
|
|
if (node.parent && node.parent.topic) {
|
|
|
|
|
|
context.push(`父节点: ${node.parent.topic}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取祖父节点信息
|
|
|
|
|
|
if (node.parent && node.parent.parent && node.parent.parent.topic) {
|
|
|
|
|
|
context.push(`祖父节点: ${node.parent.parent.topic}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return context.join(' | ');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 处理回车键事件
|
|
|
|
|
|
const handleEnterKey = (event) => {
|
|
|
|
|
|
// 如果按的是Ctrl+Enter或Meta+Enter,不处理(让原有的处理函数处理)
|
|
|
|
|
|
if (event.ctrlKey || event.metaKey) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 普通回车键:阻止默认换行行为,直接提交问题
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
submitAIQuestion();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 提交AI问题
|
|
|
|
|
|
const submitAIQuestion = async () => {
|
|
|
|
|
|
if (!aiQuestion.value.trim() || !currentQuestionNode.value || isAIProcessing.value) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
isAIProcessing.value = true;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 构建AI提示词
|
|
|
|
|
|
const systemPrompt = `你是一个专业的思维导图分析助手。请根据用户的问题和提供的节点信息,给出专业、有用的回答。`;
|
|
|
|
|
|
|
|
|
|
|
|
const userPrompt = `节点信息:
|
|
|
|
|
|
当前节点:${currentQuestionNode.value.topic}
|
|
|
|
|
|
上下文:${getNodeContext(currentQuestionNode.value)}
|
|
|
|
|
|
|
|
|
|
|
|
用户问题:${aiQuestion.value}
|
|
|
|
|
|
|
|
|
|
|
|
请给出详细的回答,回答应该:
|
|
|
|
|
|
1. 直接回答用户的问题
|
|
|
|
|
|
2. 提供具体的建议或改进方案
|
|
|
|
|
|
3. 保持专业和有用的语调
|
|
|
|
|
|
4. 回答长度适中,便于在思维导图中展示`;
|
|
|
|
|
|
|
|
|
|
|
|
console.log('发送AI请求:', { systemPrompt, userPrompt });
|
|
|
|
|
|
|
2025-09-08 10:20:48 +00:00
|
|
|
|
// 调用流式AI API
|
|
|
|
|
|
const response = await fetch('http://127.0.0.1:8000/api/ai/generate-stream', {
|
2025-09-08 07:06:08 +00:00
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
system_prompt: systemPrompt,
|
|
|
|
|
|
user_prompt: userPrompt,
|
|
|
|
|
|
model: "glm-4.5",
|
|
|
|
|
|
base_url: "https://open.bigmodel.cn/api/paas/v4/",
|
|
|
|
|
|
api_key: "ce39bdd4fcf34ec0aec75072bc9ff988.hAp7HZTVUwy7vImn"
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-08 10:20:48 +00:00
|
|
|
|
// 处理流式响应
|
2025-09-08 07:06:08 +00:00
|
|
|
|
let aiResponse = '';
|
2025-09-08 10:20:48 +00:00
|
|
|
|
const reader = response.body.getReader();
|
|
|
|
|
|
const decoder = new TextDecoder();
|
|
|
|
|
|
let buffer = '';
|
2025-09-08 07:06:08 +00:00
|
|
|
|
|
2025-09-08 10:20:48 +00:00
|
|
|
|
while (true) {
|
|
|
|
|
|
const { done, value } = await reader.read();
|
|
|
|
|
|
if (done) break;
|
|
|
|
|
|
|
|
|
|
|
|
buffer += decoder.decode(value, { stream: true });
|
|
|
|
|
|
const lines = buffer.split('\n');
|
|
|
|
|
|
buffer = lines.pop() || '';
|
|
|
|
|
|
|
|
|
|
|
|
for (const line of lines) {
|
|
|
|
|
|
if (line.startsWith('data: ')) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = JSON.parse(line.slice(6));
|
|
|
|
|
|
|
|
|
|
|
|
if (data.type === 'chunk') {
|
|
|
|
|
|
aiResponse += data.content;
|
|
|
|
|
|
// 可以在这里实时更新UI显示
|
|
|
|
|
|
} else if (data.type === 'error') {
|
|
|
|
|
|
throw new Error(data.content);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.warn('解析流式数据失败:', e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-08 07:06:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log('AI回答:', aiResponse);
|
2025-09-08 07:06:08 +00:00
|
|
|
|
|
|
|
|
|
|
// 在合适位置创建新节点展示AI回答
|
|
|
|
|
|
await createAINode(currentQuestionNode.value, aiQuestion.value, aiResponse);
|
|
|
|
|
|
|
|
|
|
|
|
// 关闭对话框
|
|
|
|
|
|
closeAIDialog();
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('AI请求失败:', error);
|
|
|
|
|
|
alert('AI请求失败,请稍后重试');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
isAIProcessing.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 创建AI回答节点
|
2025-09-08 08:51:12 +00:00
|
|
|
|
// 格式化Markdown为结构化文本
|
|
|
|
|
|
const formatMarkdownToText = (markdown) => {
|
|
|
|
|
|
return markdown
|
|
|
|
|
|
// 处理标题
|
|
|
|
|
|
.replace(/^### (.*$)/gim, '📋 $1') // 三级标题
|
|
|
|
|
|
.replace(/^## (.*$)/gim, '📌 $1') // 二级标题
|
|
|
|
|
|
.replace(/^# (.*$)/gim, '🎯 $1') // 一级标题
|
|
|
|
|
|
// 处理粗体 - 改进处理逻辑,确保冒号等标点符号正确处理
|
|
|
|
|
|
.replace(/\*\*(.*?)\*\*/g, (match, content) => {
|
|
|
|
|
|
// 如果内容包含冒号,保持冒号,只处理粗体部分
|
|
|
|
|
|
if (content.includes(':')) {
|
|
|
|
|
|
const parts = content.split(':');
|
|
|
|
|
|
if (parts.length > 1) {
|
|
|
|
|
|
return `【${parts[0]}】: ${parts.slice(1).join(':')}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return `【${content}】`;
|
|
|
|
|
|
})
|
|
|
|
|
|
// 处理斜体
|
|
|
|
|
|
.replace(/\*(.*?)\*/g, '《$1》')
|
|
|
|
|
|
// 处理列表项
|
|
|
|
|
|
.replace(/^- (.*$)/gim, ' • $1')
|
|
|
|
|
|
.replace(/^\d+\. (.*$)/gim, ' $&')
|
|
|
|
|
|
// 处理代码块
|
|
|
|
|
|
.replace(/```(.*?)```/gims, '💻 $1')
|
|
|
|
|
|
// 处理行内代码
|
|
|
|
|
|
.replace(/`(.*?)`/g, '「$1」')
|
|
|
|
|
|
// 处理链接
|
|
|
|
|
|
.replace(/\[([^\]]+)\]\([^)]+\)/g, '🔗 $1')
|
|
|
|
|
|
// 处理换行
|
|
|
|
|
|
.replace(/\n\n/g, '\n')
|
|
|
|
|
|
.replace(/\n/g, '\n ');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 智能处理内容,检测是否需要创建子节点
|
|
|
|
|
|
const processContentIntelligently = (content, parentNode, nodeCounter) => {
|
|
|
|
|
|
const lines = content.split('\n');
|
|
|
|
|
|
let currentNodeCounter = nodeCounter;
|
|
|
|
|
|
let remainingContent = [];
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
|
|
|
|
const line = lines[i];
|
|
|
|
|
|
const trimmed = line.trim();
|
|
|
|
|
|
|
|
|
|
|
|
// 检测是否是【】标记的叶节点
|
|
|
|
|
|
const leafMatch = trimmed.match(/^[-*+]\s*【(.+)】/);
|
|
|
|
|
|
if (leafMatch) {
|
|
|
|
|
|
// 创建叶节点
|
|
|
|
|
|
const leafTitle = leafMatch[1].trim();
|
|
|
|
|
|
const leafNode = {
|
|
|
|
|
|
id: `node_${currentNodeCounter++}`,
|
|
|
|
|
|
topic: leafTitle,
|
|
|
|
|
|
children: [],
|
|
|
|
|
|
level: (parentNode.level || 0) + 1,
|
|
|
|
|
|
data: {}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 收集这个叶节点的所有子项内容
|
|
|
|
|
|
let leafContent = [];
|
|
|
|
|
|
let j = i + 1;
|
|
|
|
|
|
while (j < lines.length) {
|
|
|
|
|
|
const nextLine = lines[j].trim();
|
|
|
|
|
|
const nextLeafMatch = nextLine.match(/^[-*+]\s*【(.+)】/);
|
|
|
|
|
|
|
|
|
|
|
|
if (nextLeafMatch) {
|
|
|
|
|
|
// 遇到下一个【】标记,停止收集
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 收集子项内容
|
|
|
|
|
|
const listMatch = nextLine.match(/^[-*+]\s+(.+)$/);
|
|
|
|
|
|
if (listMatch) {
|
|
|
|
|
|
const listItem = listMatch[1].trim();
|
|
|
|
|
|
const cleanListItem = formatMarkdownToText(listItem);
|
|
|
|
|
|
leafContent.push('• ' + cleanListItem);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
j++;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果有子项内容,添加到叶节点
|
|
|
|
|
|
if (leafContent.length > 0) {
|
|
|
|
|
|
const formattedContent = formatMarkdownToText(leafContent.join('\n'));
|
|
|
|
|
|
leafNode.topic = leafNode.topic + '\n\n' + formattedContent;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
parentNode.children.push(leafNode);
|
|
|
|
|
|
|
|
|
|
|
|
// 跳过已处理的行
|
|
|
|
|
|
i = j - 1;
|
|
|
|
|
|
} else if (trimmed) {
|
|
|
|
|
|
// 普通内容,累积到剩余内容中
|
|
|
|
|
|
remainingContent.push(trimmed);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果有剩余内容,按换行切分成并列的子节点
|
|
|
|
|
|
if (remainingContent.length > 0) {
|
|
|
|
|
|
const finalContent = remainingContent.join('\n').trim();
|
|
|
|
|
|
if (finalContent) {
|
|
|
|
|
|
// 按换行符切分内容
|
|
|
|
|
|
const paragraphs = finalContent.split('\n\n').filter(p => p.trim());
|
|
|
|
|
|
|
|
|
|
|
|
paragraphs.forEach(paragraph => {
|
|
|
|
|
|
const cleanParagraph = formatMarkdownToText(paragraph.trim());
|
|
|
|
|
|
if (cleanParagraph) {
|
|
|
|
|
|
const paragraphNode = {
|
|
|
|
|
|
id: `node_${currentNodeCounter++}`,
|
|
|
|
|
|
topic: cleanParagraph,
|
|
|
|
|
|
children: [],
|
|
|
|
|
|
level: (parentNode.level || 0) + 1,
|
|
|
|
|
|
data: {}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
parentNode.children.push(paragraphNode);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return { nodeCounter: currentNodeCounter };
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Markdown转JSON的核心逻辑 - 智能层次化版本
|
|
|
|
|
|
const markdownToJSON = (markdown) => {
|
|
|
|
|
|
const lines = markdown.split('\n');
|
|
|
|
|
|
let root = null;
|
|
|
|
|
|
const stack = [];
|
|
|
|
|
|
let nodeCounter = 0;
|
|
|
|
|
|
let currentContent = [];
|
|
|
|
|
|
|
|
|
|
|
|
lines.forEach((line, index) => {
|
|
|
|
|
|
const trimmed = line.trim();
|
|
|
|
|
|
|
|
|
|
|
|
// 检测标题级别
|
|
|
|
|
|
const match = trimmed.match(/^(#{1,6})\s+(.+)$/);
|
|
|
|
|
|
if (match) {
|
|
|
|
|
|
// 如果有累积的内容,先保存到当前节点
|
|
|
|
|
|
if (currentContent.length > 0 && stack.length > 0) {
|
|
|
|
|
|
const content = currentContent.join('\n').trim();
|
|
|
|
|
|
if (content) {
|
|
|
|
|
|
// 智能处理内容:检测是否需要创建子节点
|
|
|
|
|
|
const processedContent = processContentIntelligently(content, stack[stack.length - 1], nodeCounter);
|
|
|
|
|
|
nodeCounter = processedContent.nodeCounter;
|
|
|
|
|
|
}
|
|
|
|
|
|
currentContent = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const level = match[1].length;
|
|
|
|
|
|
const title = match[2].trim();
|
|
|
|
|
|
|
|
|
|
|
|
// 清理标题中的Markdown语法
|
|
|
|
|
|
const cleanTitle = formatMarkdownToText(title);
|
|
|
|
|
|
|
|
|
|
|
|
// 创建节点
|
|
|
|
|
|
const node = {
|
|
|
|
|
|
id: `node_${nodeCounter++}`,
|
|
|
|
|
|
topic: cleanTitle,
|
|
|
|
|
|
children: [],
|
|
|
|
|
|
level: level,
|
|
|
|
|
|
data: {}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 如果是第一个节点(最高级别),设为根节点
|
|
|
|
|
|
if (level === 1 && !root) {
|
|
|
|
|
|
root = node;
|
|
|
|
|
|
stack.length = 0; // 清空栈
|
|
|
|
|
|
stack.push(root);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 找到合适的父节点
|
|
|
|
|
|
while (stack.length > 1 && stack[stack.length - 1].level >= level) {
|
|
|
|
|
|
stack.pop();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加到父节点
|
|
|
|
|
|
if (stack.length > 0) {
|
|
|
|
|
|
stack[stack.length - 1].children.push(node);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新栈
|
|
|
|
|
|
stack.push(node);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (trimmed) {
|
|
|
|
|
|
// 累积内容行
|
|
|
|
|
|
currentContent.push(trimmed);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 处理最后的内容
|
|
|
|
|
|
if (currentContent.length > 0 && stack.length > 0) {
|
|
|
|
|
|
const content = currentContent.join('\n').trim();
|
|
|
|
|
|
if (content) {
|
|
|
|
|
|
const processedContent = processContentIntelligently(content, stack[stack.length - 1], nodeCounter);
|
|
|
|
|
|
nodeCounter = processedContent.nodeCounter;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果没有找到任何内容,返回默认根节点
|
|
|
|
|
|
if (!root) {
|
|
|
|
|
|
root = {
|
|
|
|
|
|
id: 'root',
|
|
|
|
|
|
topic: '根节点',
|
|
|
|
|
|
children: [],
|
|
|
|
|
|
data: {}
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return root;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-08 07:06:08 +00:00
|
|
|
|
const createAINode = async (parentNode, question, answer) => {
|
|
|
|
|
|
try {
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log('开始创建AI节点...', { parentNode, question, answer });
|
2025-09-08 07:06:08 +00:00
|
|
|
|
|
|
|
|
|
|
// 使用现有的Markdown转JSON逻辑
|
|
|
|
|
|
const formatAnswer = (text) => {
|
|
|
|
|
|
return text
|
|
|
|
|
|
.replace(/^#+\s*/gm, '') // 移除标题标记
|
|
|
|
|
|
.replace(/\*\*(.*?)\*\*/g, '$1') // 移除粗体标记
|
|
|
|
|
|
.replace(/\*(.*?)\*/g, '$1') // 移除斜体标记
|
2025-09-10 10:26:48 +00:00
|
|
|
|
// 保留表格格式,不转换表格为列表
|
|
|
|
|
|
.replace(/^\s*[-*+]\s*(?![|])/gm, '• ') // 只转换非表格的列表标记
|
2025-09-08 07:06:08 +00:00
|
|
|
|
.replace(/\n{3,}/g, '\n\n') // 限制连续换行
|
|
|
|
|
|
.trim();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const formattedAnswer = formatAnswer(answer);
|
|
|
|
|
|
|
2025-09-08 08:51:12 +00:00
|
|
|
|
// 使用Markdown转JSON逻辑处理AI回复
|
|
|
|
|
|
const aiMarkdown = `# ${question}\n\n${formattedAnswer}`;
|
|
|
|
|
|
const aiJSON = markdownToJSON(aiMarkdown);
|
|
|
|
|
|
|
|
|
|
|
|
// 创建AI回复的父节点
|
|
|
|
|
|
const aiParentNode = {
|
|
|
|
|
|
title: question,
|
|
|
|
|
|
des: `AI追问产生的节点 - ${new Date().toLocaleString()}`,
|
2025-09-08 07:06:08 +00:00
|
|
|
|
parentId: parentNode.id,
|
|
|
|
|
|
isRoot: false
|
|
|
|
|
|
};
|
2025-09-08 08:51:12 +00:00
|
|
|
|
|
|
|
|
|
|
// 准备子节点数据
|
|
|
|
|
|
const childNodes = [];
|
|
|
|
|
|
if (aiJSON.children && aiJSON.children.length > 0) {
|
|
|
|
|
|
aiJSON.children.forEach(child => {
|
|
|
|
|
|
childNodes.push({
|
|
|
|
|
|
title: child.topic,
|
|
|
|
|
|
des: '',
|
|
|
|
|
|
parentId: null, // 将在创建父节点后设置
|
|
|
|
|
|
isRoot: false
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-09-08 07:06:08 +00:00
|
|
|
|
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log('准备创建AI父节点:', aiParentNode);
|
|
|
|
|
|
// console.log('准备创建AI子节点:', childNodes);
|
2025-09-08 07:06:08 +00:00
|
|
|
|
console.log('当前思维导图ID:', currentMindmapId.value);
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否有思维导图ID
|
|
|
|
|
|
if (!currentMindmapId.value) {
|
|
|
|
|
|
throw new Error('没有找到当前思维导图ID,无法创建节点');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-08 08:51:12 +00:00
|
|
|
|
// 先创建父节点
|
|
|
|
|
|
const parentResponse = await mindmapAPI.addNodes(currentMindmapId.value, [aiParentNode]);
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log('AI父节点创建响应:', parentResponse);
|
2025-09-08 08:51:12 +00:00
|
|
|
|
|
|
|
|
|
|
if (!parentResponse.data || !parentResponse.data.success) {
|
|
|
|
|
|
throw new Error('AI父节点创建失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取创建的父节点ID
|
|
|
|
|
|
const createdParentId = parentResponse.data.data?.nodes?.[0]?.id;
|
|
|
|
|
|
if (!createdParentId) {
|
|
|
|
|
|
throw new Error('无法获取创建的父节点ID');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 设置子节点的父节点ID
|
|
|
|
|
|
childNodes.forEach(child => {
|
|
|
|
|
|
child.parentId = createdParentId;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 创建子节点
|
|
|
|
|
|
let childResponse = null;
|
|
|
|
|
|
if (childNodes.length > 0) {
|
|
|
|
|
|
childResponse = await mindmapAPI.addNodes(currentMindmapId.value, childNodes);
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log('AI子节点创建响应:', childResponse);
|
2025-09-08 08:51:12 +00:00
|
|
|
|
}
|
2025-09-08 07:06:08 +00:00
|
|
|
|
|
2025-09-08 08:51:12 +00:00
|
|
|
|
if (parentResponse.data && parentResponse.data.success) {
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log('✅ AI节点创建成功,开始刷新思维导图...');
|
2025-09-08 07:06:08 +00:00
|
|
|
|
|
|
|
|
|
|
// 刷新思维导图以显示新节点
|
|
|
|
|
|
await refreshMindMap();
|
|
|
|
|
|
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log('✅ AI节点创建并刷新完成');
|
2025-09-08 07:06:08 +00:00
|
|
|
|
} else {
|
2025-09-08 08:51:12 +00:00
|
|
|
|
throw new Error('AI父节点创建失败');
|
2025-09-08 07:06:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('创建AI节点失败:', error);
|
|
|
|
|
|
alert('创建AI回答节点失败: ' + error.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-04 05:47:42 +00:00
|
|
|
|
const copyNodeText = async () => {
|
|
|
|
|
|
if (!selectedNode.value) return;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 获取节点的文本内容
|
|
|
|
|
|
const textToCopy = selectedNode.value.topic || selectedNode.value.title || "无标题";
|
|
|
|
|
|
|
|
|
|
|
|
// 使用现代浏览器的 Clipboard API
|
|
|
|
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
|
|
|
|
await navigator.clipboard.writeText(textToCopy);
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log("文本已复制到剪贴板:", textToCopy);
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
// 显示成功提示
|
|
|
|
|
|
showCopySuccess();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 降级方案:使用传统的 document.execCommand
|
|
|
|
|
|
const textArea = document.createElement("textarea");
|
|
|
|
|
|
textArea.value = textToCopy;
|
|
|
|
|
|
textArea.style.position = "fixed";
|
|
|
|
|
|
textArea.style.left = "-999999px";
|
|
|
|
|
|
textArea.style.top = "-999999px";
|
|
|
|
|
|
document.body.appendChild(textArea);
|
|
|
|
|
|
textArea.focus();
|
|
|
|
|
|
textArea.select();
|
|
|
|
|
|
|
|
|
|
|
|
const successful = document.execCommand('copy');
|
|
|
|
|
|
document.body.removeChild(textArea);
|
|
|
|
|
|
|
|
|
|
|
|
if (successful) {
|
|
|
|
|
|
showCopySuccess();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showCopyError();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
showCopyError();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 操作完成后隐藏菜单
|
|
|
|
|
|
selectedNode.value = null;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 显示复制成功提示
|
|
|
|
|
|
const showCopySuccess = () => {
|
|
|
|
|
|
// 创建临时提示元素
|
|
|
|
|
|
const notification = document.createElement('div');
|
|
|
|
|
|
notification.textContent = '文本已复制到剪贴板';
|
|
|
|
|
|
notification.style.cssText = `
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
top: 20px;
|
|
|
|
|
|
right: 20px;
|
|
|
|
|
|
background: #4CAF50;
|
|
|
|
|
|
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;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// 添加动画样式
|
|
|
|
|
|
const style = document.createElement('style');
|
|
|
|
|
|
style.textContent = `
|
|
|
|
|
|
@keyframes slideIn {
|
|
|
|
|
|
from { transform: translateX(100%); opacity: 0; }
|
|
|
|
|
|
to { transform: translateX(0); opacity: 1; }
|
|
|
|
|
|
}
|
|
|
|
|
|
`;
|
|
|
|
|
|
document.head.appendChild(style);
|
|
|
|
|
|
|
|
|
|
|
|
document.body.appendChild(notification);
|
|
|
|
|
|
|
|
|
|
|
|
// 3秒后自动移除
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (notification.parentNode) {
|
|
|
|
|
|
notification.parentNode.removeChild(notification);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (style.parentNode) {
|
|
|
|
|
|
style.parentNode.removeChild(style);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 3000);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 显示复制失败提示
|
|
|
|
|
|
const showCopyError = () => {
|
|
|
|
|
|
const notification = document.createElement('div');
|
|
|
|
|
|
notification.textContent = '复制失败,请手动复制';
|
|
|
|
|
|
notification.style.cssText = `
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
top: 20px;
|
|
|
|
|
|
right: 20px;
|
|
|
|
|
|
background: #f44336;
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 3000);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// 通用通知函数
|
|
|
|
|
|
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);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-04 05:47:42 +00:00
|
|
|
|
// 显示编辑成功通知
|
|
|
|
|
|
const showEditSuccessNotification = () => {
|
|
|
|
|
|
const notification = document.createElement('div');
|
|
|
|
|
|
notification.textContent = '✅ 节点编辑已保存';
|
|
|
|
|
|
notification.style.cssText = `
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
top: 20px;
|
|
|
|
|
|
right: 20px;
|
|
|
|
|
|
background: #4CAF50;
|
|
|
|
|
|
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 showEditErrorNotification = () => {
|
|
|
|
|
|
const notification = document.createElement('div');
|
|
|
|
|
|
notification.textContent = '❌ 节点编辑保存失败';
|
|
|
|
|
|
notification.style.cssText = `
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
top: 20px;
|
|
|
|
|
|
right: 20px;
|
|
|
|
|
|
background: #f44336;
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 3000);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 处理节点拖拽
|
|
|
|
|
|
const handleNodeDrop = async (node, targetNode) => {
|
|
|
|
|
|
try {
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log("处理节点拖拽:", {
|
|
|
|
|
|
// nodeId: node.id,
|
|
|
|
|
|
// nodeTopic: node.topic,
|
|
|
|
|
|
// targetNodeId: targetNode?.id,
|
|
|
|
|
|
// targetNodeTopic: targetNode?.topic
|
|
|
|
|
|
// });
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
// 确定新的父节点ID
|
|
|
|
|
|
const newParentId = targetNode ? targetNode.id : null;
|
|
|
|
|
|
|
|
|
|
|
|
// 调用后端API更新节点
|
|
|
|
|
|
await updateNodeParent(node, newParentId);
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
// 静默处理错误
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 处理节点移动
|
|
|
|
|
|
const handleNodeMove = async (node, oldParent, newParent) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 确定新的父节点ID
|
|
|
|
|
|
const newParentId = newParent ? newParent.id : null;
|
|
|
|
|
|
|
|
|
|
|
|
// 调用后端API更新节点
|
|
|
|
|
|
await updateNodeParent(node, newParentId);
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
// 静默处理错误
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// 处理节点拖拽操作(更新节点的父子关系)
|
|
|
|
|
|
const handleNodeDragOperation = async (operation) => {
|
2025-09-04 05:47:42 +00:00
|
|
|
|
try {
|
2025-10-09 06:20:51 +00:00
|
|
|
|
console.log("🎯 处理节点拖拽操作:", operation.name);
|
|
|
|
|
|
console.log("📦 操作详情:", {
|
|
|
|
|
|
name: operation.name,
|
|
|
|
|
|
objs: operation.objs,
|
|
|
|
|
|
toObj: operation.toObj
|
|
|
|
|
|
});
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// 延迟保存,等待MindElixir更新完DOM
|
|
|
|
|
|
setTimeout(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const movedNodes = operation.objs || []; // 被移动的节点数组
|
|
|
|
|
|
const targetNode = operation.toObj; // 目标节点
|
|
|
|
|
|
|
|
|
|
|
|
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更新完成
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
2025-10-09 06:20:51 +00:00
|
|
|
|
console.error('❌ 处理节点拖拽操作失败:', error);
|
2025-09-04 05:47:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 处理编辑完成
|
|
|
|
|
|
const handleEditFinish = async (operation) => {
|
|
|
|
|
|
try {
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log("处理编辑完成:", operation);
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
const editedNode = operation.obj; // 被编辑的节点
|
|
|
|
|
|
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log("编辑详情:", {
|
|
|
|
|
|
// editedNode: editedNode,
|
|
|
|
|
|
// nodeId: editedNode?.id,
|
|
|
|
|
|
// nodeTopic: editedNode?.topic
|
|
|
|
|
|
// });
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
if (editedNode) {
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log("保存编辑的节点:", {
|
|
|
|
|
|
// nodeId: editedNode.id,
|
|
|
|
|
|
// nodeTopic: editedNode.topic,
|
|
|
|
|
|
// mindmapId: editedNode.mindmapId || editedNode.mindmap_id
|
|
|
|
|
|
// });
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
// 调用后端API更新节点
|
|
|
|
|
|
await updateNodeEdit(editedNode);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.error("无法解析编辑操作:", operation);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("处理编辑完成失败:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 更新节点编辑
|
|
|
|
|
|
const updateNodeEdit = async (node) => {
|
|
|
|
|
|
try {
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log("更新节点编辑:", {
|
|
|
|
|
|
// nodeId: node.id,
|
|
|
|
|
|
// nodeTopic: node.topic,
|
|
|
|
|
|
// currentMindmapId: currentMindmapId.value
|
|
|
|
|
|
// });
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
// 使用全局的思维导图ID
|
|
|
|
|
|
if (!currentMindmapId.value) {
|
|
|
|
|
|
console.error("无法获取思维导图ID");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 调用后端API
|
|
|
|
|
|
const response = await mindmapAPI.updateNode(node.id, {
|
|
|
|
|
|
newTitle: node.topic,
|
|
|
|
|
|
newDes: node.data?.des || "",
|
|
|
|
|
|
newParentId: node.parentId || node.parent?.id
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log("更新节点编辑响应:", response);
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
if (response.data && response.data.success) {
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log("✅ 节点编辑更新成功");
|
2025-09-04 05:47:42 +00:00
|
|
|
|
// 显示成功通知
|
|
|
|
|
|
showEditSuccessNotification();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.error("更新节点编辑失败:", response.data);
|
|
|
|
|
|
showEditErrorNotification();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("更新节点编辑失败:", error);
|
|
|
|
|
|
showEditErrorNotification();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 更新节点父节点
|
|
|
|
|
|
const updateNodeParent = async (node, newParentId) => {
|
|
|
|
|
|
try {
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log("更新节点父节点:", {
|
|
|
|
|
|
// nodeId: node.id,
|
|
|
|
|
|
// newParentId: newParentId,
|
|
|
|
|
|
// mindmapId: node.mindmapId || node.mindmap_id
|
|
|
|
|
|
// });
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
// 获取思维导图ID
|
|
|
|
|
|
const mindmapId = node.mindmapId || node.mindmap_id;
|
|
|
|
|
|
if (!mindmapId) {
|
|
|
|
|
|
console.error("无法获取思维导图ID");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 调用后端API
|
|
|
|
|
|
const response = await mindmapAPI.updateNode(node.id, {
|
|
|
|
|
|
newTitle: node.topic,
|
|
|
|
|
|
newDes: node.data?.des || "",
|
|
|
|
|
|
newParentId: newParentId
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log("更新节点父节点响应:", response);
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
if (response.data && response.data.success) {
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log("✅ 节点父节点更新成功");
|
2025-09-04 05:47:42 +00:00
|
|
|
|
// 不需要重新加载整个思维导图,只需要更新本地数据
|
|
|
|
|
|
// 这样可以避免用户看到数据被重置
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.error("更新节点父节点失败:", response.data);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("更新节点父节点失败:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 添加子节点(预览模式和已保存模式都可以编辑)
|
|
|
|
|
|
const addChildNodeToAPI = async (parentNode) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 先保存当前编辑
|
|
|
|
|
|
await saveCurrentEdit();
|
|
|
|
|
|
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log("添加子节点到API:", parentNode.id);
|
|
|
|
|
|
// console.log("父节点信息:", {
|
|
|
|
|
|
// id: parentNode.id,
|
|
|
|
|
|
// mindmap_id: parentNode.mindmap_id,
|
|
|
|
|
|
// mindmapId: parentNode.mindmapId
|
|
|
|
|
|
// });
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
// 获取思维导图ID
|
|
|
|
|
|
const mindmapId = currentMindmapId.value || parentNode.mindmap_id || parentNode.mindmapId;
|
|
|
|
|
|
if (!mindmapId) {
|
|
|
|
|
|
console.error("无法获取思维导图ID");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 已保存的思维导图,调用后端API
|
|
|
|
|
|
const response = await mindmapAPI.addNodes(mindmapId, [{
|
|
|
|
|
|
title: "新子节点",
|
|
|
|
|
|
des: "子节点描述",
|
|
|
|
|
|
parentId: parentNode.id
|
|
|
|
|
|
}]);
|
|
|
|
|
|
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log("添加子节点响应:", response);
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
if (response.data && response.data.success) {
|
|
|
|
|
|
// 获取新创建的节点数据
|
|
|
|
|
|
const createdNodes = response.data.data?.nodes || [];
|
|
|
|
|
|
if (createdNodes.length > 0) {
|
|
|
|
|
|
const newNode = createdNodes[0];
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log("新创建的子节点:", newNode);
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
// 使用可靠的loadMindmapData方法刷新思维导图
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log("🎯 使用loadMindmapData刷新思维导图显示新节点...");
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 延迟一下让用户看到提示
|
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 800));
|
|
|
|
|
|
|
|
|
|
|
|
// 重新获取最新的思维导图数据
|
|
|
|
|
|
const mindmapResponse = await mindmapAPI.getMindmap(mindmapId);
|
|
|
|
|
|
if (mindmapResponse.data && mindmapResponse.data.nodeData) {
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// 使用可靠的loadMindmapData方法,不保持位置,不居中根节点(因为我们要居中新节点)
|
|
|
|
|
|
await loadMindmapData(mindmapResponse.data, false, false);
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log("✅ 思维导图刷新成功");
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// 立即居中显示新节点并进入编辑模式,不延迟
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log('🎯 开始居中显示新子节点:', newNode.id);
|
|
|
|
|
|
await centerNodeAndEdit(newNode.id);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("居中显示新节点失败:", error);
|
|
|
|
|
|
}
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error("无法获取思维导图数据");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("刷新思维导图失败:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("添加子节点失败:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const addSiblingNodeToAPI = async (node) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 先保存当前编辑
|
|
|
|
|
|
await saveCurrentEdit();
|
|
|
|
|
|
|
|
|
|
|
|
console.log("添加兄弟节点到API:", node.id);
|
|
|
|
|
|
console.log("节点信息:", {
|
|
|
|
|
|
id: node.id,
|
|
|
|
|
|
parentId: node.parentId,
|
|
|
|
|
|
parent: node.parent,
|
|
|
|
|
|
mindmap_id: node.mindmap_id,
|
|
|
|
|
|
mindmapId: node.mindmapId
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 获取思维导图ID
|
|
|
|
|
|
const mindmapId = currentMindmapId.value || node.mindmap_id || node.mindmapId;
|
|
|
|
|
|
if (!mindmapId) {
|
|
|
|
|
|
console.error("无法获取思维导图ID");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 删除预览模式特殊处理,统一使用后端API
|
|
|
|
|
|
|
|
|
|
|
|
// 确定父节点ID
|
|
|
|
|
|
let parentId = node.parentId;
|
|
|
|
|
|
if (!parentId && node.parent) {
|
|
|
|
|
|
parentId = node.parent.id;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const response = await mindmapAPI.addNodes(mindmapId, [{
|
|
|
|
|
|
title: "新兄弟节点",
|
|
|
|
|
|
des: "兄弟节点描述",
|
|
|
|
|
|
parentId: parentId
|
|
|
|
|
|
}]);
|
|
|
|
|
|
|
|
|
|
|
|
console.log("添加兄弟节点响应:", response);
|
|
|
|
|
|
|
|
|
|
|
|
if (response.data && response.data.success) {
|
|
|
|
|
|
// 获取新创建的节点数据
|
|
|
|
|
|
const createdNodes = response.data.data?.nodes || [];
|
|
|
|
|
|
if (createdNodes.length > 0) {
|
|
|
|
|
|
const newNode = createdNodes[0];
|
|
|
|
|
|
console.log("新创建的兄弟节点:", newNode);
|
|
|
|
|
|
|
|
|
|
|
|
// 使用MindElixir的init方法重新初始化数据,确保新节点显示
|
|
|
|
|
|
console.log("🎯 使用MindElixir init方法重新初始化数据...");
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 重新获取最新的思维导图数据
|
|
|
|
|
|
const mindmapResponse = await mindmapAPI.getMindmap(mindmapId);
|
|
|
|
|
|
if (mindmapResponse.data && mindmapResponse.data.nodeData) {
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// 使用可靠的loadMindmapData方法,不保持位置,不居中根节点(因为我们要居中新节点)
|
|
|
|
|
|
await loadMindmapData(mindmapResponse.data, false, false);
|
2025-09-04 05:47:42 +00:00
|
|
|
|
console.log("✅ 思维导图刷新成功");
|
|
|
|
|
|
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// 立即居中显示新节点并进入编辑模式,不延迟
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log('🎯 开始居中显示新兄弟节点:', newNode.id);
|
|
|
|
|
|
await centerNodeAndEdit(newNode.id);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("居中显示新节点失败:", error);
|
|
|
|
|
|
}
|
2025-09-04 05:47:42 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error("无法获取思维导图数据");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("重新初始化失败,使用完整重新加载:", error);
|
|
|
|
|
|
|
|
|
|
|
|
// 最终回退方案:完整重新加载
|
|
|
|
|
|
const mindmapResponse = await mindmapAPI.getMindmap(mindmapId);
|
|
|
|
|
|
if (mindmapResponse.data && mindmapResponse.data.nodeData) {
|
2025-10-09 06:20:51 +00:00
|
|
|
|
await loadMindmapData(mindmapResponse.data, true, false);
|
2025-09-04 05:47:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("添加兄弟节点失败:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const deleteNodeFromAPI = async (node) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 先保存当前编辑
|
|
|
|
|
|
await saveCurrentEdit();
|
|
|
|
|
|
|
|
|
|
|
|
console.log("删除节点从API:", node.id);
|
|
|
|
|
|
const response = await mindmapAPI.deleteNodes([node.id]);
|
|
|
|
|
|
|
|
|
|
|
|
console.log("删除节点响应:", response);
|
|
|
|
|
|
|
|
|
|
|
|
if (response.data && response.data.success) {
|
|
|
|
|
|
// 获取思维导图ID
|
|
|
|
|
|
const mindmapId = currentMindmapId.value || node.mindmap_id || node.mindmapId;
|
|
|
|
|
|
if (mindmapId) {
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// 重新加载数据,保持当前位置,不居中根节点
|
2025-09-04 05:47:42 +00:00
|
|
|
|
const mindmapResponse = await mindmapAPI.getMindmap(mindmapId);
|
|
|
|
|
|
if (mindmapResponse.data && mindmapResponse.data.nodeData) {
|
2025-10-09 06:20:51 +00:00
|
|
|
|
await loadMindmapData(mindmapResponse.data, true, false); // 保持当前位置,不居中根节点
|
|
|
|
|
|
console.log("✅ 删除节点后思维导图已刷新,保持当前位置");
|
2025-09-04 05:47:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("删除节点失败:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 保存当前编辑
|
|
|
|
|
|
const saveCurrentEdit = async () => {
|
|
|
|
|
|
const mindmapContainer = mindmapEl.value;
|
|
|
|
|
|
if (mindmapContainer) {
|
|
|
|
|
|
const activeInput = mindmapContainer.querySelector('input:focus');
|
|
|
|
|
|
if (activeInput && selectedNode.value) {
|
|
|
|
|
|
const newTitle = activeInput.value;
|
|
|
|
|
|
if (newTitle !== selectedNode.value.topic && newTitle.trim() !== '') {
|
|
|
|
|
|
console.log("保存当前编辑:", newTitle);
|
|
|
|
|
|
selectedNode.value.topic = newTitle;
|
|
|
|
|
|
await saveNodeEditToAPI(selectedNode.value);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 保存节点编辑到API
|
|
|
|
|
|
const saveNodeEditToAPI = async (node) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log("保存Mind Elixir编辑,节点ID:", node.id);
|
|
|
|
|
|
console.log("编辑内容:", {
|
|
|
|
|
|
newTitle: node.topic,
|
|
|
|
|
|
newDes: node.data?.des || ""
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 确保ID是字符串格式
|
|
|
|
|
|
const nodeId = String(node.id);
|
|
|
|
|
|
|
|
|
|
|
|
const response = await mindmapAPI.updateNode(nodeId, {
|
|
|
|
|
|
newTitle: node.topic,
|
|
|
|
|
|
newDes: node.data?.des || ""
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
console.log("更新节点API响应:", response);
|
|
|
|
|
|
// 不重新加载数据,避免循环调用
|
|
|
|
|
|
console.log("节点编辑已保存到后端");
|
|
|
|
|
|
|
|
|
|
|
|
// 显示成功通知
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 不需要重新加载整个思维导图,避免用户看到数据被重置
|
|
|
|
|
|
console.log("✅ 节点编辑已保存,无需重新加载");
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("保存Mind Elixir编辑失败:", error);
|
|
|
|
|
|
console.error("错误详情:", error.response?.data);
|
|
|
|
|
|
alert("保存编辑失败: " + (error.response?.data?.detail || error.message));
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 打开编辑模态框
|
|
|
|
|
|
const openEditModal = (node) => {
|
|
|
|
|
|
console.log("打开编辑模态框:", node);
|
|
|
|
|
|
// 触发Mind Elixir的编辑模式
|
|
|
|
|
|
mindElixir.value.editText(node);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 删除导入功能,只保留预览和保存功能
|
|
|
|
|
|
|
|
|
|
|
|
// 删除预览相关功能,统一使用保存到数据库的方式
|
|
|
|
|
|
|
|
|
|
|
|
// 递归创建节点
|
|
|
|
|
|
const createNodesRecursively = async (node, mindmapId, parentId) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log("开始创建节点:", node.topic, "父节点ID:", parentId);
|
|
|
|
|
|
|
|
|
|
|
|
// 创建当前节点
|
2025-10-10 05:36:34 +00:00
|
|
|
|
const nodeData = {
|
2025-09-04 05:47:42 +00:00
|
|
|
|
title: node.topic || node.title || "无标题",
|
2025-09-08 07:06:08 +00:00
|
|
|
|
des: node.data?.des || "",
|
2025-09-04 05:47:42 +00:00
|
|
|
|
parentId: parentId
|
2025-10-10 05:36:34 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 如果有HTML内容,添加到节点数据中
|
|
|
|
|
|
if (node.dangerouslySetInnerHTML) {
|
|
|
|
|
|
nodeData.dangerouslySetInnerHTML = node.dangerouslySetInnerHTML;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const nodeResponse = await mindmapAPI.addNodes(mindmapId, nodeData);
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
console.log("创建节点响应:", nodeResponse);
|
|
|
|
|
|
|
|
|
|
|
|
if (nodeResponse.data && nodeResponse.data.success) {
|
|
|
|
|
|
// 从响应中获取新创建的节点ID
|
|
|
|
|
|
const createdNodes = nodeResponse.data.data?.nodes || [];
|
|
|
|
|
|
const currentNodeId = createdNodes[0]?.id; // 获取第一个创建的节点ID
|
|
|
|
|
|
|
|
|
|
|
|
console.log("当前节点ID:", currentNodeId);
|
|
|
|
|
|
|
|
|
|
|
|
// 递归创建子节点
|
|
|
|
|
|
if (node.children && node.children.length > 0) {
|
|
|
|
|
|
console.log("开始创建子节点,数量:", node.children.length);
|
|
|
|
|
|
for (const child of node.children) {
|
|
|
|
|
|
await createNodesRecursively(child, mindmapId, currentNodeId);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log("节点没有子节点,创建完成");
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.error("创建节点失败,响应:", nodeResponse);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("创建节点失败:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 绑定事件监听器
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// 保存事件监听器引用,用于清理
|
|
|
|
|
|
let wheelHandler = null;
|
|
|
|
|
|
let clickHandler = null;
|
|
|
|
|
|
|
2025-09-04 05:47:42 +00:00
|
|
|
|
const bindEventListeners = () => {
|
|
|
|
|
|
if (!mindElixir.value) return;
|
|
|
|
|
|
|
|
|
|
|
|
console.log("绑定事件监听器...");
|
|
|
|
|
|
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// ✅ 先移除旧的事件监听器,避免重复绑定
|
|
|
|
|
|
if (wheelHandler) {
|
|
|
|
|
|
mindmapEl.value.removeEventListener('wheel', wheelHandler);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (clickHandler) {
|
|
|
|
|
|
mindmapEl.value.removeEventListener('click', clickHandler);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-04 05:47:42 +00:00
|
|
|
|
// 添加鼠标滚轮缩放功能
|
2025-10-09 06:20:51 +00:00
|
|
|
|
wheelHandler = (event) => {
|
2025-09-04 05:47:42 +00:00
|
|
|
|
if (event.ctrlKey || event.metaKey) {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
|
|
|
|
|
|
const delta = event.deltaY > 0 ? 0.9 : 1.1;
|
|
|
|
|
|
const newZoom = Math.max(0.3, Math.min(3, zoomLevel.value * delta));
|
|
|
|
|
|
|
|
|
|
|
|
if (mindElixir.value) {
|
|
|
|
|
|
const mapContainer = mindmapEl.value.querySelector('.map-container');
|
|
|
|
|
|
if (mapContainer) {
|
|
|
|
|
|
mapContainer.style.transform = `scale(${newZoom})`;
|
|
|
|
|
|
zoomLevel.value = newZoom;
|
|
|
|
|
|
// 保存缩放状态
|
|
|
|
|
|
localStorage.setItem('mindmap-zoom-level', newZoom.toString());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-09 06:20:51 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
mindmapEl.value.addEventListener('wheel', wheelHandler);
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
// 重新绑定事件监听器
|
|
|
|
|
|
mindElixir.value.bus.addListener("select", (node) => {
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// console.log("select事件触发:", node); // ✅ 注释掉频繁日志
|
2025-09-04 05:47:42 +00:00
|
|
|
|
selectedNode.value = node;
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// 延迟计算菜单位置,确保DOM已更新
|
2025-09-04 05:47:42 +00:00
|
|
|
|
setTimeout(() => {
|
2025-10-09 06:20:51 +00:00
|
|
|
|
calculateMenuPosition();
|
|
|
|
|
|
}, 50);
|
2025-09-04 05:47:42 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
mindElixir.value.bus.addListener("selectNode", (node) => {
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// console.log("selectNode事件触发:", node); // ✅ 注释掉频繁日志
|
2025-09-04 05:47:42 +00:00
|
|
|
|
selectedNode.value = node;
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// 延迟计算菜单位置,确保DOM已更新
|
2025-09-04 05:47:42 +00:00
|
|
|
|
setTimeout(() => {
|
2025-10-09 06:20:51 +00:00
|
|
|
|
calculateMenuPosition();
|
|
|
|
|
|
}, 50);
|
2025-09-04 05:47:42 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 监听缩放变化,保持缩放状态
|
|
|
|
|
|
mindElixir.value.bus.addListener("scale", (scale) => {
|
|
|
|
|
|
// 如果缩放被重置,重新应用保存的缩放级别
|
|
|
|
|
|
if (Math.abs(scale - 1) < 0.01 && Math.abs(zoomLevel.value - 1) > 0.01) {
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
applySavedZoom();
|
|
|
|
|
|
}, 50);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// 添加定时器来持续保持缩放状态(降低频率)
|
|
|
|
|
|
// 清理旧的定时器,避免重复
|
|
|
|
|
|
if (window.zoomIntervalId) {
|
|
|
|
|
|
clearInterval(window.zoomIntervalId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-04 05:47:42 +00:00
|
|
|
|
const zoomInterval = setInterval(() => {
|
|
|
|
|
|
if (zoomLevel.value !== 1 && mindmapEl.value) {
|
|
|
|
|
|
maintainZoomLevel();
|
|
|
|
|
|
}
|
2025-10-09 06:20:51 +00:00
|
|
|
|
}, 500); // ✅ 改为500ms,降低CPU占用
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
// 保存定时器ID,以便后续清理
|
|
|
|
|
|
window.zoomIntervalId = zoomInterval;
|
|
|
|
|
|
|
|
|
|
|
|
// 添加点击事件监听器
|
|
|
|
|
|
let lastClickTime = 0;
|
2025-10-09 06:20:51 +00:00
|
|
|
|
clickHandler = (event) => {
|
2025-09-04 05:47:42 +00:00
|
|
|
|
const currentTime = Date.now();
|
|
|
|
|
|
if (currentTime - lastClickTime < 100) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
lastClickTime = currentTime;
|
|
|
|
|
|
|
|
|
|
|
|
const clickedElement = event.target;
|
2025-10-09 06:20:51 +00:00
|
|
|
|
const topicElement = clickedElement.closest('me-tpc') ||
|
|
|
|
|
|
clickedElement.closest('.topic') ||
|
|
|
|
|
|
(clickedElement.classList.contains('topic') ? clickedElement : null) ||
|
|
|
|
|
|
(clickedElement.tagName === 'ME-TPC' ? clickedElement : null);
|
|
|
|
|
|
|
|
|
|
|
|
if (topicElement) {
|
|
|
|
|
|
// 点击了节点,获取节点对象并设置为选中
|
|
|
|
|
|
const nodeObj = topicElement.nodeObj;
|
|
|
|
|
|
if (nodeObj) {
|
|
|
|
|
|
selectedNode.value = nodeObj;
|
|
|
|
|
|
|
|
|
|
|
|
// 计算菜单位置(在节点下方居中)
|
|
|
|
|
|
const containerRect = mindmapEl.value.getBoundingClientRect();
|
|
|
|
|
|
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`
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// console.log('✅ 节点被点击,显示自定义菜单:', nodeObj.topic); // ✅ 注释掉频繁日志
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 点击空白区域,隐藏菜单
|
2025-09-04 05:47:42 +00:00
|
|
|
|
selectedNode.value = null;
|
2025-09-08 07:06:08 +00:00
|
|
|
|
// 点击空白区域时也关闭AI询问框
|
|
|
|
|
|
if (showAIDialog.value) {
|
|
|
|
|
|
showAIDialog.value = false;
|
|
|
|
|
|
currentQuestionNode.value = null;
|
|
|
|
|
|
aiQuestion.value = '';
|
|
|
|
|
|
isAIProcessing.value = false;
|
|
|
|
|
|
}
|
2025-09-04 05:47:42 +00:00
|
|
|
|
}
|
2025-10-09 06:20:51 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
mindmapEl.value.addEventListener('click', clickHandler);
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
mindElixir.value.bus.addListener("edit", (node) => {
|
|
|
|
|
|
console.log("edit事件触发:", node);
|
|
|
|
|
|
openEditModal(node);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
mindElixir.value.bus.addListener("editFinish", (operation) => {
|
|
|
|
|
|
console.log("editFinish事件触发:", operation);
|
|
|
|
|
|
handleEditFinish(operation);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// 监听操作事件 - 监听拖拽和编辑操作
|
2025-09-04 05:47:42 +00:00
|
|
|
|
mindElixir.value.bus.addListener("operation", (operation) => {
|
|
|
|
|
|
console.log("Mind Elixir操作事件:", operation);
|
|
|
|
|
|
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// 监听拖拽移动节点事件
|
|
|
|
|
|
if (operation.name === 'moveNodeIn' || operation.name === 'moveNodeBefore' || operation.name === 'moveNodeAfter') {
|
|
|
|
|
|
console.log("检测到节点拖拽操作:", operation.name, operation);
|
|
|
|
|
|
handleNodeDragOperation(operation);
|
2025-09-04 05:47:42 +00:00
|
|
|
|
} else if (operation.name === 'finishEdit') {
|
|
|
|
|
|
console.log("检测到编辑完成操作:", operation);
|
|
|
|
|
|
handleEditFinish(operation);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// 禁用 MindElixir 内置的添加节点事件监听,因为我们使用自定义的右键菜单
|
|
|
|
|
|
// mindElixir.value.bus.addListener("addChild", (node) => {
|
|
|
|
|
|
// console.log("添加子节点:", node);
|
|
|
|
|
|
// addChildNodeToAPI(node);
|
|
|
|
|
|
// });
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// mindElixir.value.bus.addListener("addSibling", (node) => {
|
|
|
|
|
|
// console.log("添加兄弟节点:", node);
|
|
|
|
|
|
// addSiblingNodeToAPI(node);
|
|
|
|
|
|
// });
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
mindElixir.value.bus.addListener("removeNode", (node) => {
|
|
|
|
|
|
console.log("删除节点:", node);
|
|
|
|
|
|
deleteNodeFromAPI(node);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// console.log("事件监听器绑定完成");
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 缩放功能
|
|
|
|
|
|
const zoomIn = () => {
|
|
|
|
|
|
if (mindElixir.value) {
|
|
|
|
|
|
const newZoom = Math.min(zoomLevel.value * 1.2, 3); // 最大放大到300%
|
|
|
|
|
|
const mapContainer = mindmapEl.value.querySelector('.map-container');
|
|
|
|
|
|
if (mapContainer) {
|
|
|
|
|
|
mapContainer.style.transform = `scale(${newZoom})`;
|
|
|
|
|
|
zoomLevel.value = newZoom;
|
|
|
|
|
|
// 保存缩放状态
|
|
|
|
|
|
localStorage.setItem('mindmap-zoom-level', newZoom.toString());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const zoomOut = () => {
|
|
|
|
|
|
if (mindElixir.value) {
|
|
|
|
|
|
const newZoom = Math.max(zoomLevel.value / 1.2, 0.3); // 最小缩小到30%
|
|
|
|
|
|
const mapContainer = mindmapEl.value.querySelector('.map-container');
|
|
|
|
|
|
if (mapContainer) {
|
|
|
|
|
|
mapContainer.style.transform = `scale(${newZoom})`;
|
|
|
|
|
|
zoomLevel.value = newZoom;
|
|
|
|
|
|
// 保存缩放状态
|
|
|
|
|
|
localStorage.setItem('mindmap-zoom-level', newZoom.toString());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const resetZoom = () => {
|
|
|
|
|
|
if (mindElixir.value) {
|
|
|
|
|
|
const mapContainer = mindmapEl.value.querySelector('.map-container');
|
|
|
|
|
|
if (mapContainer) {
|
|
|
|
|
|
mapContainer.style.transform = 'scale(1)';
|
|
|
|
|
|
zoomLevel.value = 1;
|
|
|
|
|
|
// 保存缩放状态
|
|
|
|
|
|
localStorage.setItem('mindmap-zoom-level', '1');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 应用保存的缩放级别
|
|
|
|
|
|
const applySavedZoom = () => {
|
|
|
|
|
|
if (mindElixir.value) {
|
|
|
|
|
|
const savedZoom = localStorage.getItem('mindmap-zoom-level');
|
|
|
|
|
|
if (savedZoom) {
|
|
|
|
|
|
const zoom = parseFloat(savedZoom);
|
|
|
|
|
|
if (zoom >= 0.3 && zoom <= 3) {
|
|
|
|
|
|
// 直接操作DOM来设置缩放
|
|
|
|
|
|
const mapContainer = mindmapEl.value.querySelector('.map-container');
|
|
|
|
|
|
if (mapContainer) {
|
|
|
|
|
|
mapContainer.style.transform = `scale(${zoom})`;
|
|
|
|
|
|
zoomLevel.value = zoom;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 强制保持缩放状态
|
|
|
|
|
|
const maintainZoomLevel = () => {
|
|
|
|
|
|
if (mindElixir.value && zoomLevel.value !== 1 && mindmapEl.value) {
|
|
|
|
|
|
const mapContainer = mindmapEl.value.querySelector('.map-container');
|
|
|
|
|
|
if (mapContainer) {
|
|
|
|
|
|
mapContainer.style.transform = `scale(${zoomLevel.value})`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 保存思维导图
|
|
|
|
|
|
const saveMindMap = async () => {
|
|
|
|
|
|
console.log("🚀🚀🚀 保存函数被调用 🚀🚀🚀");
|
|
|
|
|
|
console.log("🔍 mindElixir.value:", mindElixir.value);
|
|
|
|
|
|
console.log("🔍 currentMindmapId.value:", currentMindmapId.value);
|
|
|
|
|
|
console.log("🔍 mindmapEl.value:", mindmapEl.value);
|
|
|
|
|
|
|
|
|
|
|
|
// 检查全局状态
|
|
|
|
|
|
console.log("🔍🔍🔍 全局状态检查开始 🔍🔍🔍");
|
|
|
|
|
|
console.log("🔍 - showWelcome:", showWelcome.value);
|
|
|
|
|
|
console.log("🔍 - 是否有思维导图容器:", !!mindmapEl.value);
|
|
|
|
|
|
console.log("🔍 - MindElixir实例状态:", !!mindElixir.value);
|
|
|
|
|
|
console.log("🔍🔍🔍 全局状态检查结束 🔍🔍🔍");
|
|
|
|
|
|
|
|
|
|
|
|
// 检查MindElixir数据
|
|
|
|
|
|
if (mindElixir.value && mindElixir.value.data) {
|
|
|
|
|
|
console.log("🔍🔍🔍 MindElixir数据检查开始 🔍🔍🔍");
|
|
|
|
|
|
console.log("🔍 - 数据对象:", mindElixir.value.data);
|
|
|
|
|
|
console.log("🔍 - 数据字段:", Object.keys(mindElixir.value.data));
|
|
|
|
|
|
console.log("🔍 - 是否有nodeData:", !!mindElixir.value.data.nodeData);
|
|
|
|
|
|
console.log("🔍 - 是否有nodes:", !!mindElixir.value.data.nodes);
|
|
|
|
|
|
console.log("🔍🔍🔍 MindElixir数据检查结束 🔍🔍🔍");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log("⚠️ MindElixir数据不存在或为空");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!mindElixir.value || !currentMindmapId.value) {
|
|
|
|
|
|
console.warn("⚠️ 没有可保存的思维导图数据");
|
|
|
|
|
|
console.warn("🔍 mindElixir.value 状态:", !!mindElixir.value);
|
|
|
|
|
|
console.warn("🔍 currentMindmapId.value 状态:", !!currentMindmapId.value);
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试从其他地方获取思维导图ID
|
|
|
|
|
|
if (mindElixir.value && mindElixir.value.data) {
|
|
|
|
|
|
console.log("🔍 尝试从MindElixir数据中获取ID");
|
|
|
|
|
|
console.log("🔍 MindElixir数据:", mindElixir.value.data);
|
|
|
|
|
|
|
|
|
|
|
|
if (mindElixir.value.data.mindmapId) {
|
|
|
|
|
|
currentMindmapId.value = mindElixir.value.data.mindmapId;
|
|
|
|
|
|
console.log("🔍 从MindElixir数据中获取到mindmapId:", mindElixir.value.data.mindmapId);
|
|
|
|
|
|
} else if (mindElixir.value.data.mindmap_id) {
|
|
|
|
|
|
currentMindmapId.value = mindElixir.value.data.mindmap_id;
|
|
|
|
|
|
console.log("🔍 从MindElixir数据中获取到mindmap_id:", mindElixir.value.data.mindmap_id);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果还是没有数据,尝试重新初始化
|
|
|
|
|
|
if (!currentMindmapId.value && mindElixir.value) {
|
|
|
|
|
|
console.log("🔍 尝试重新初始化MindElixir数据...");
|
|
|
|
|
|
|
|
|
|
|
|
// 检查MindElixir实例的所有属性
|
|
|
|
|
|
console.log("🔍 MindElixir实例属性:", Object.keys(mindElixir.value));
|
|
|
|
|
|
console.log("🔍 MindElixir实例方法:", Object.getOwnPropertyNames(Object.getPrototypeOf(mindElixir.value)));
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试获取数据
|
|
|
|
|
|
if (mindElixir.value.getData) {
|
|
|
|
|
|
const data = mindElixir.value.getData();
|
|
|
|
|
|
console.log("🔍 通过getData()获取的数据:", data);
|
|
|
|
|
|
|
|
|
|
|
|
// 检查nodeData中是否有思维导图ID
|
|
|
|
|
|
if (data && data.nodeData) {
|
|
|
|
|
|
console.log("🔍 nodeData详情:", data.nodeData);
|
|
|
|
|
|
|
|
|
|
|
|
// 检查nodeData是否有mindmapId字段(不要使用nodeData.id,那是节点ID)
|
|
|
|
|
|
if (data.nodeData.mindmapId) {
|
|
|
|
|
|
currentMindmapId.value = data.nodeData.mindmapId;
|
|
|
|
|
|
console.log("🔍 从nodeData.mindmapId获取到ID:", data.nodeData.mindmapId);
|
|
|
|
|
|
} else if (data.nodeData.mindmap_id) {
|
|
|
|
|
|
currentMindmapId.value = data.nodeData.mindmap_id;
|
|
|
|
|
|
console.log("🔍 从nodeData.mindmap_id获取到ID:", data.nodeData.mindmap_id);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log("🔍 nodeData中没有找到mindmapId字段");
|
|
|
|
|
|
console.log("🔍 nodeData字段:", Object.keys(data.nodeData));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果还是没有,尝试从其他地方获取
|
|
|
|
|
|
if (!currentMindmapId.value && data) {
|
|
|
|
|
|
console.log("🔍 检查其他可能的ID字段...");
|
|
|
|
|
|
if (data.id) {
|
|
|
|
|
|
currentMindmapId.value = data.id;
|
|
|
|
|
|
console.log("🔍 从data.id获取到ID:", data.id);
|
|
|
|
|
|
} else if (data.mindmapId) {
|
|
|
|
|
|
currentMindmapId.value = data.mindmapId;
|
|
|
|
|
|
console.log("🔍 从data.mindmapId获取到ID:", data.mindmapId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否有其他数据属性
|
|
|
|
|
|
if (mindElixir.value.mindElixirData) {
|
|
|
|
|
|
console.log("🔍 mindElixirData:", mindElixir.value.mindElixirData);
|
|
|
|
|
|
if (mindElixir.value.mindElixirData.id) {
|
|
|
|
|
|
currentMindmapId.value = mindElixir.value.mindElixirData.id;
|
|
|
|
|
|
console.log("🔍 从mindElixirData获取到ID:", mindElixir.value.mindElixirData.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 再次检查
|
|
|
|
|
|
if (!currentMindmapId.value) {
|
|
|
|
|
|
console.warn("⚠️ 仍然无法获取思维导图ID,无法保存");
|
|
|
|
|
|
console.warn("🔍 建议:请先加载一个思维导图,然后再尝试保存");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log("💾 开始保存思维导图...");
|
|
|
|
|
|
console.log("🔍 当前思维导图ID:", currentMindmapId.value);
|
|
|
|
|
|
console.log("🔍 MindElixir实例:", mindElixir.value);
|
|
|
|
|
|
|
|
|
|
|
|
// 获取当前思维导图数据 - 使用getData()方法而不是data属性
|
|
|
|
|
|
let currentData = null;
|
|
|
|
|
|
if (mindElixir.value && mindElixir.value.getData) {
|
|
|
|
|
|
currentData = mindElixir.value.getData();
|
|
|
|
|
|
console.log("🔍 通过getData()获取的数据:", currentData);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
currentData = mindElixir.value.data;
|
|
|
|
|
|
console.log("🔍 通过data属性获取的数据:", currentData);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!currentData) {
|
|
|
|
|
|
console.warn("⚠️ MindElixir数据为空");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查数据结构
|
|
|
|
|
|
let allNodes = [];
|
|
|
|
|
|
console.log("🔍 数据结构分析:");
|
|
|
|
|
|
console.log("🔍 - currentData.nodeData:", currentData.nodeData);
|
|
|
|
|
|
console.log("🔍 - currentData.nodes:", currentData.nodes);
|
|
|
|
|
|
console.log("🔍 - currentData.nodeData.children:", currentData.nodeData?.children);
|
|
|
|
|
|
|
|
|
|
|
|
// 收集所有节点,包括根节点和子节点
|
|
|
|
|
|
const collectAllNodes = (node) => {
|
|
|
|
|
|
if (node && node.id) {
|
|
|
|
|
|
allNodes.push(node);
|
|
|
|
|
|
console.log(`🔍 收集节点: ${node.id} - ${node.topic || node.content || '无标题'}`);
|
|
|
|
|
|
|
|
|
|
|
|
// 递归收集子节点
|
|
|
|
|
|
if (node.children && node.children.length > 0) {
|
|
|
|
|
|
node.children.forEach(child => collectAllNodes(child));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (currentData.nodeData) {
|
|
|
|
|
|
// 从根节点开始收集所有节点
|
|
|
|
|
|
collectAllNodes(currentData.nodeData);
|
|
|
|
|
|
console.log("🔍 从根节点收集到节点数量:", allNodes.length);
|
|
|
|
|
|
} else if (currentData.nodes && Array.isArray(currentData.nodes)) {
|
|
|
|
|
|
// 如果nodes是数组,直接收集
|
|
|
|
|
|
currentData.nodes.forEach(node => collectAllNodes(node));
|
|
|
|
|
|
console.log("🔍 从nodes数组收集到节点数量:", allNodes.length);
|
|
|
|
|
|
} else if (Array.isArray(currentData)) {
|
|
|
|
|
|
// 如果currentData本身就是数组
|
|
|
|
|
|
currentData.forEach(node => collectAllNodes(node));
|
|
|
|
|
|
console.log("🔍 从currentData数组收集到节点数量:", allNodes.length);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.warn("⚠️ 无法找到节点数据");
|
|
|
|
|
|
console.log("🔍 可用的数据字段:", Object.keys(currentData));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (allNodes.length === 0) {
|
|
|
|
|
|
console.warn("⚠️ 没有收集到任何节点");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log("🔍 找到节点数据:", allNodes);
|
|
|
|
|
|
console.log("🔍 节点数量:", allNodes.length);
|
|
|
|
|
|
console.log("🔍 节点数据类型:", Array.isArray(allNodes) ? "数组" : typeof allNodes);
|
|
|
|
|
|
|
|
|
|
|
|
// 遍历所有节点,保存内容和位置
|
|
|
|
|
|
const updatePromises = [];
|
|
|
|
|
|
|
|
|
|
|
|
allNodes.forEach((node, index) => {
|
|
|
|
|
|
console.log(`🔍 处理节点 ${index}:`, node);
|
|
|
|
|
|
|
|
|
|
|
|
if (node && node.id) {
|
|
|
|
|
|
// 获取节点内容
|
|
|
|
|
|
const content = node.topic || node.content || node.text || '';
|
|
|
|
|
|
const x = node.x || node.offsetX || 0;
|
|
|
|
|
|
const y = node.y || node.offsetY || 0;
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`🔍 节点 ${node.id} 内容:`, content, "位置:", { x, y });
|
|
|
|
|
|
|
|
|
|
|
|
// 保存节点内容
|
|
|
|
|
|
const updateData = {
|
|
|
|
|
|
content: content,
|
|
|
|
|
|
position: {
|
|
|
|
|
|
x: x,
|
|
|
|
|
|
y: y
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 如果有父节点,保存父子关系
|
|
|
|
|
|
if (node.parentId || node.parent) {
|
|
|
|
|
|
updateData.parentId = node.parentId || node.parent;
|
|
|
|
|
|
console.log(`🔍 节点 ${node.id} 父节点:`, updateData.parentId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`🔍 准备保存节点 ${node.id}:`, updateData);
|
|
|
|
|
|
|
|
|
|
|
|
updatePromises.push(
|
|
|
|
|
|
mindmapAPI.updateNode(node.id, updateData)
|
|
|
|
|
|
.then((response) => {
|
|
|
|
|
|
console.log(`✅ 节点 ${node.id} 保存成功:`, response);
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(error => {
|
|
|
|
|
|
console.error(`❌ 节点 ${node.id} 保存失败:`, error);
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.warn(`⚠️ 节点 ${index} 缺少ID:`, node);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
console.log("🔍 准备保存的节点数量:", updatePromises.length);
|
|
|
|
|
|
|
|
|
|
|
|
if (updatePromises.length === 0) {
|
|
|
|
|
|
console.warn("⚠️ 没有找到需要保存的节点");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 等待所有保存操作完成
|
|
|
|
|
|
const results = await Promise.all(updatePromises);
|
|
|
|
|
|
console.log("🔍 保存结果:", results);
|
|
|
|
|
|
|
|
|
|
|
|
console.log("🎉 思维导图保存完成!");
|
|
|
|
|
|
|
|
|
|
|
|
// 保存完成后自动刷新思维导图
|
|
|
|
|
|
console.log("🔄 保存完成,开始自动刷新...");
|
|
|
|
|
|
await refreshMindMap();
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("❌ 保存思维导图失败:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 刷新思维导图
|
|
|
|
|
|
const refreshMindMap = async () => {
|
|
|
|
|
|
if (!currentMindmapId.value) {
|
|
|
|
|
|
console.warn("⚠️ 没有当前思维导图ID,无法刷新");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log("🔄 开始刷新思维导图...");
|
|
|
|
|
|
|
|
|
|
|
|
// 从后端重新获取最新的思维导图数据
|
|
|
|
|
|
const response = await mindmapAPI.getMindmap(currentMindmapId.value);
|
|
|
|
|
|
if (response.data && response.data.nodeData) {
|
|
|
|
|
|
console.log("✅ 获取到最新数据,开始刷新显示...");
|
|
|
|
|
|
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// 使用loadMindmapData重新加载,保持当前位置,不居中根节点
|
|
|
|
|
|
await loadMindmapData(response.data, true, false);
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
console.log("🎉 思维导图刷新完成!");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.warn("⚠️ 无法获取思维导图数据");
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("❌ 刷新思维导图失败:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 保存预览数据到数据库
|
|
|
|
|
|
const savePreviewToDatabase = async (data, title) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 开始保存预览数据到数据库
|
|
|
|
|
|
|
2025-09-10 05:02:45 +00:00
|
|
|
|
// 检查是否已经有实时渲染的思维导图
|
|
|
|
|
|
const currentId = String(currentMindmapId.value || '');
|
|
|
|
|
|
const hasRealtimeMindmap = mindElixir.value && currentId && currentId.startsWith('temp-');
|
|
|
|
|
|
|
|
|
|
|
|
if (hasRealtimeMindmap) {
|
|
|
|
|
|
console.log("🔄 检测到实时渲染的思维导图,将保存到数据库并更新ID");
|
|
|
|
|
|
|
|
|
|
|
|
// 保存当前位置和缩放状态
|
|
|
|
|
|
const currentPosition = saveCurrentPosition();
|
|
|
|
|
|
console.log("📍 保存当前位置:", currentPosition);
|
|
|
|
|
|
|
|
|
|
|
|
// 创建新的思维导图
|
|
|
|
|
|
const response = await mindmapAPI.createMindmap(title || "预览思维导图", data);
|
|
|
|
|
|
console.log("🔄 创建思维导图响应:", response);
|
|
|
|
|
|
|
|
|
|
|
|
if (response.data && response.data.id) {
|
|
|
|
|
|
const newMindmapId = response.data.id;
|
|
|
|
|
|
console.log("🎉 创建思维导图成功,新思维导图的ID是:", newMindmapId);
|
|
|
|
|
|
|
|
|
|
|
|
// 更新当前思维导图ID,但保持视图状态
|
|
|
|
|
|
const oldMindmapId = currentMindmapId.value;
|
|
|
|
|
|
currentMindmapId.value = newMindmapId;
|
|
|
|
|
|
|
|
|
|
|
|
// 更新MindElixir数据中的ID
|
|
|
|
|
|
if (mindElixir.value && mindElixir.value.data) {
|
|
|
|
|
|
mindElixir.value.data.mindmapId = newMindmapId;
|
|
|
|
|
|
mindElixir.value.data.id = newMindmapId;
|
|
|
|
|
|
|
|
|
|
|
|
// 更新所有节点的mindmapId
|
|
|
|
|
|
const updateNodeIds = (node) => {
|
|
|
|
|
|
if (node) {
|
|
|
|
|
|
node.mindmapId = newMindmapId;
|
|
|
|
|
|
if (node.children) {
|
|
|
|
|
|
node.children.forEach(child => updateNodeIds(child));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (mindElixir.value.data.nodeData) {
|
|
|
|
|
|
updateNodeIds(mindElixir.value.data.nodeData);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log("✅ 已更新思维导图ID,保持视图状态");
|
|
|
|
|
|
console.log("🔄 从临时ID", oldMindmapId, "更新为正式ID", newMindmapId);
|
|
|
|
|
|
|
|
|
|
|
|
// 恢复位置和缩放状态
|
|
|
|
|
|
if (currentPosition) {
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
restorePosition(currentPosition);
|
|
|
|
|
|
console.log("📍 已恢复位置和缩放状态");
|
|
|
|
|
|
}, 100);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 通知AISidebar组件更新历史记录
|
|
|
|
|
|
window.dispatchEvent(new CustomEvent('mindmap-saved', {
|
|
|
|
|
|
detail: {
|
|
|
|
|
|
mindmapId: newMindmapId,
|
|
|
|
|
|
title: title,
|
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
|
fromRealtime: true // 标记来自实时渲染
|
|
|
|
|
|
}
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果没有实时渲染的思维导图,使用原有逻辑
|
|
|
|
|
|
console.log("🔄 没有检测到实时渲染的思维导图,使用标准保存流程");
|
|
|
|
|
|
|
2025-09-04 05:47:42 +00:00
|
|
|
|
// 创建新的思维导图
|
|
|
|
|
|
const response = await mindmapAPI.createMindmap(title || "预览思维导图", data);
|
|
|
|
|
|
console.log("🔄 创建思维导图响应:", response);
|
|
|
|
|
|
|
|
|
|
|
|
if (response.data && response.data.id) {
|
|
|
|
|
|
const mindmapId = response.data.id;
|
|
|
|
|
|
console.log("🎉 创建思维导图成功,新思维导图的ID是:", mindmapId);
|
|
|
|
|
|
console.log("📊 响应数据详情:", response.data);
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否已经有节点数据
|
|
|
|
|
|
if (response.data.nodeData) {
|
|
|
|
|
|
console.log("✅ 思维导图创建时已包含节点数据,直接加载");
|
|
|
|
|
|
currentMindmapId.value = mindmapId;
|
|
|
|
|
|
|
|
|
|
|
|
// 强制隐藏欢迎页面
|
|
|
|
|
|
hideWelcomePage();
|
|
|
|
|
|
|
|
|
|
|
|
await loadMindmapData(response.data);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 通知AISidebar组件更新历史记录,保存真实的思维导图ID
|
|
|
|
|
|
window.dispatchEvent(new CustomEvent('mindmap-saved', {
|
|
|
|
|
|
detail: {
|
|
|
|
|
|
mindmapId: mindmapId,
|
|
|
|
|
|
title: title,
|
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
|
}
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
// 延迟后重新加载思维导图数据
|
|
|
|
|
|
setTimeout(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log("🔄 重新加载思维导图数据...");
|
|
|
|
|
|
const refreshResponse = await mindmapAPI.getMindmap(mindmapId);
|
|
|
|
|
|
if (refreshResponse.data && refreshResponse.data.nodeData) {
|
|
|
|
|
|
await loadMindmapData(refreshResponse.data);
|
|
|
|
|
|
console.log("✅ 思维导图数据重新加载成功");
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("❌ 重新加载思维导图失败:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 1500);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log("🔧 需要递归创建节点");
|
|
|
|
|
|
// 递归创建节点
|
|
|
|
|
|
await createNodesRecursively(data, mindmapId, null);
|
|
|
|
|
|
|
|
|
|
|
|
// 立即加载并显示新保存的思维导图
|
|
|
|
|
|
console.log("📥 开始加载新创建的思维导图,ID:", mindmapId);
|
|
|
|
|
|
const mindmapResponse = await mindmapAPI.getMindmap(mindmapId);
|
|
|
|
|
|
console.log("🔄 加载思维导图响应:", mindmapResponse);
|
|
|
|
|
|
|
|
|
|
|
|
if (mindmapResponse.data && mindmapResponse.data.nodeData) {
|
|
|
|
|
|
console.log("✅ 成功获取思维导图数据,开始加载显示");
|
|
|
|
|
|
currentMindmapId.value = mindmapId;
|
|
|
|
|
|
|
|
|
|
|
|
// 强制隐藏欢迎页面
|
|
|
|
|
|
hideWelcomePage();
|
|
|
|
|
|
|
|
|
|
|
|
await loadMindmapData(mindmapResponse.data);
|
|
|
|
|
|
|
|
|
|
|
|
// 显示成功通知
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 通知AISidebar组件更新历史记录,保存真实的思维导图ID
|
|
|
|
|
|
window.dispatchEvent(new CustomEvent('mindmap-saved', {
|
|
|
|
|
|
detail: {
|
|
|
|
|
|
mindmapId: mindmapId,
|
|
|
|
|
|
title: title,
|
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
|
}
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
// 延迟后重新加载思维导图数据
|
|
|
|
|
|
setTimeout(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log("🔄 重新加载思维导图数据...");
|
|
|
|
|
|
const refreshResponse = await mindmapAPI.getMindmap(mindmapId);
|
|
|
|
|
|
if (refreshResponse.data && refreshResponse.data.nodeData) {
|
|
|
|
|
|
await loadMindmapData(refreshResponse.data);
|
|
|
|
|
|
console.log("✅ 思维导图数据重新加载成功");
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("❌ 重新加载思维导图失败:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 1500);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.error("❌ 获取思维导图数据失败:", mindmapResponse);
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("❌ 保存预览数据到数据库失败:", error);
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 从历史记录加载思维导图
|
|
|
|
|
|
const loadMindmapFromHistory = async (historyData) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log('📚 开始从历史记录加载思维导图:', historyData.title);
|
|
|
|
|
|
|
|
|
|
|
|
// 隐藏欢迎页面
|
|
|
|
|
|
hideWelcomePage();
|
|
|
|
|
|
|
|
|
|
|
|
// 如果有思维导图ID,直接调用后端接口
|
|
|
|
|
|
if (historyData.mindmapId) {
|
|
|
|
|
|
console.log('🎯 使用思维导图ID加载:', historyData.mindmapId);
|
|
|
|
|
|
|
|
|
|
|
|
// 调用后端API获取思维导图数据
|
|
|
|
|
|
const response = await mindmapAPI.getMindmap(historyData.mindmapId);
|
|
|
|
|
|
if (response.data && response.data.nodeData) {
|
|
|
|
|
|
await loadMindmapData(response.data);
|
|
|
|
|
|
// 移除成功通知,避免重复显示
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error('无法获取思维导图数据');
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (historyData.json) {
|
|
|
|
|
|
console.log('📊 使用历史记录中的JSON数据');
|
|
|
|
|
|
await loadMindmapData(historyData.json);
|
|
|
|
|
|
// 移除成功通知,避免重复显示
|
|
|
|
|
|
} else if (historyData.markdown) {
|
|
|
|
|
|
console.log('📝 使用历史记录中的Markdown数据,尝试转换为JSON');
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 将Markdown转换为JSON格式
|
|
|
|
|
|
const jsonData = await convertMarkdownToJSON(historyData.markdown);
|
|
|
|
|
|
if (jsonData) {
|
|
|
|
|
|
await loadMindmapData(jsonData);
|
|
|
|
|
|
// 移除成功通知,避免重复显示
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error('Markdown转换失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Markdown转换失败:', error);
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('❌ 从历史记录加载思维导图失败:', error);
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 显示成功通知
|
|
|
|
|
|
const showSuccessNotification = (message) => {
|
|
|
|
|
|
const notification = document.createElement('div');
|
|
|
|
|
|
notification.textContent = message;
|
|
|
|
|
|
notification.style.cssText = `
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
top: 20px;
|
|
|
|
|
|
right: 20px;
|
|
|
|
|
|
background: #4CAF50;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
padding: 12px 20px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
z-index: 10000;
|
|
|
|
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
|
|
|
|
animation: slideIn 0.3s ease;
|
|
|
|
|
|
max-width: 300px;
|
|
|
|
|
|
word-wrap: break-word;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// 添加动画样式
|
|
|
|
|
|
if (!document.querySelector('#notification-styles')) {
|
|
|
|
|
|
const style = document.createElement('style');
|
|
|
|
|
|
style.id = 'notification-styles';
|
|
|
|
|
|
style.textContent = `
|
|
|
|
|
|
@keyframes slideIn {
|
|
|
|
|
|
from { transform: translateX(100%); opacity: 0; }
|
|
|
|
|
|
to { transform: translateX(0); opacity: 1; }
|
|
|
|
|
|
}
|
|
|
|
|
|
@keyframes slideOut {
|
|
|
|
|
|
from { transform: translateX(0); opacity: 1; }
|
|
|
|
|
|
to { transform: translateX(100%); opacity: 0; }
|
|
|
|
|
|
}
|
|
|
|
|
|
`;
|
|
|
|
|
|
document.head.appendChild(style);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
document.body.appendChild(notification);
|
|
|
|
|
|
|
|
|
|
|
|
// 3秒后自动移除
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
notification.style.animation = 'slideOut 0.3s ease';
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (notification.parentNode) {
|
|
|
|
|
|
notification.parentNode.removeChild(notification);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 300);
|
|
|
|
|
|
}, 3000);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 显示错误通知
|
|
|
|
|
|
const showErrorNotification = (message) => {
|
|
|
|
|
|
const notification = document.createElement('div');
|
|
|
|
|
|
notification.textContent = message;
|
|
|
|
|
|
notification.style.cssText = `
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
top: 20px;
|
|
|
|
|
|
right: 20px;
|
|
|
|
|
|
background: #f44336;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
padding: 12px 20px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
z-index: 10000;
|
|
|
|
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
|
|
|
|
animation: slideIn 0.3s ease;
|
|
|
|
|
|
max-width: 300px;
|
|
|
|
|
|
word-wrap: break-word;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
document.body.appendChild(notification);
|
|
|
|
|
|
|
|
|
|
|
|
// 3秒后自动移除
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
notification.style.animation = 'slideOut 0.3s ease';
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (notification.parentNode) {
|
|
|
|
|
|
notification.parentNode.removeChild(notification);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 300);
|
|
|
|
|
|
}, 3000);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 组件挂载时初始化
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
// 监听保存到数据库事件
|
|
|
|
|
|
window.addEventListener('save-preview-to-database', (event) => {
|
|
|
|
|
|
console.log('🎯 收到保存到数据库事件:', event.detail);
|
|
|
|
|
|
console.log('📋 事件详情 - 标题:', event.detail.title, '来源:', event.detail.source, '时间戳:', new Date(event.detail.timestamp).toLocaleString());
|
|
|
|
|
|
savePreviewToDatabase(event.detail.data, event.detail.title);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-08 10:20:48 +00:00
|
|
|
|
// 🎯 新增:监听实时思维导图更新事件
|
|
|
|
|
|
window.addEventListener('realtime-mindmap-update', (event) => {
|
|
|
|
|
|
console.log('🔄 收到实时思维导图更新事件:', event.detail);
|
|
|
|
|
|
updateMindMapRealtime(event.detail.data, event.detail.title);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-04 05:47:42 +00:00
|
|
|
|
// 监听从历史记录加载思维导图事件
|
|
|
|
|
|
window.addEventListener('loadMindmapFromHistory', (event) => {
|
|
|
|
|
|
console.log('📚 收到从历史记录加载思维导图事件:', event.detail);
|
|
|
|
|
|
console.log('🔍 事件数据详情:', event.detail);
|
|
|
|
|
|
loadMindmapFromHistory(event.detail);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 监听AI侧边栏折叠状态变化
|
|
|
|
|
|
window.addEventListener('ai-sidebar-toggle', (event) => {
|
|
|
|
|
|
console.log('🤖 AI侧边栏折叠状态变化:', event.detail.isCollapsed);
|
|
|
|
|
|
isAISidebarCollapsed.value = event.detail.isCollapsed;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 显示欢迎页面,而不是尝试加载特定的思维导图
|
|
|
|
|
|
showWelcomePage();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 清理定时器
|
|
|
|
|
|
const cleanupIntervals = () => {
|
2025-10-09 06:20:51 +00:00
|
|
|
|
// 清理定时器
|
2025-09-04 05:47:42 +00:00
|
|
|
|
if (window.zoomIntervalId) {
|
|
|
|
|
|
clearInterval(window.zoomIntervalId);
|
|
|
|
|
|
window.zoomIntervalId = null;
|
|
|
|
|
|
}
|
2025-10-09 06:20:51 +00:00
|
|
|
|
|
|
|
|
|
|
// 清理事件监听器
|
|
|
|
|
|
if (mindmapEl.value) {
|
|
|
|
|
|
if (wheelHandler) {
|
|
|
|
|
|
mindmapEl.value.removeEventListener('wheel', wheelHandler);
|
|
|
|
|
|
wheelHandler = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (clickHandler) {
|
|
|
|
|
|
mindmapEl.value.removeEventListener('click', clickHandler);
|
|
|
|
|
|
clickHandler = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log('✅ 已清理所有定时器和事件监听器');
|
2025-09-04 05:47:42 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 组件卸载时清理定时器
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
cleanupIntervals();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 编辑完成后刷新思维导图的函数
|
|
|
|
|
|
const refreshMindmapAfterEdit = async (mindmapId, savedPosition) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log("🔄 编辑完成,开始刷新思维导图...");
|
|
|
|
|
|
|
|
|
|
|
|
// 保存当前位置(如果没有传入保存的位置)
|
|
|
|
|
|
const currentPosition = savedPosition || saveCurrentPosition();
|
|
|
|
|
|
|
|
|
|
|
|
// 重新获取最新的思维导图数据
|
|
|
|
|
|
const mindmapResponse = await mindmapAPI.getMindmap(mindmapId);
|
|
|
|
|
|
if (mindmapResponse.data && mindmapResponse.data.nodeData) {
|
|
|
|
|
|
// 使用MindElixir的init方法重新初始化,而不是重新创建实例
|
|
|
|
|
|
if (mindElixir.value && mindElixir.value.init) {
|
|
|
|
|
|
// 重新初始化数据
|
|
|
|
|
|
mindElixir.value.init(mindmapResponse.data.nodeData);
|
|
|
|
|
|
console.log("✅ MindElixir数据重新初始化成功");
|
|
|
|
|
|
|
|
|
|
|
|
// 恢复位置
|
|
|
|
|
|
if (currentPosition) {
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
restorePosition(currentPosition);
|
|
|
|
|
|
console.log("✅ 位置已恢复");
|
|
|
|
|
|
}, 300);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log("✅ 思维导图刷新完成");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error("MindElixir实例或init方法不可用");
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error("无法获取思维导图数据");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("刷新思维导图失败:", error);
|
|
|
|
|
|
|
|
|
|
|
|
// 回退方案:完整重新加载
|
|
|
|
|
|
try {
|
|
|
|
|
|
const mindmapResponse = await mindmapAPI.getMindmap(mindmapId);
|
|
|
|
|
|
if (mindmapResponse.data && mindmapResponse.data.nodeData) {
|
2025-10-09 06:20:51 +00:00
|
|
|
|
await loadMindmapData(mindmapResponse.data, true, false);
|
2025-09-04 05:47:42 +00:00
|
|
|
|
console.log("✅ 使用回退方案刷新成功");
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (fallbackError) {
|
|
|
|
|
|
console.error("回退方案也失败:", fallbackError);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// 带重试机制的渲染函数
|
|
|
|
|
|
const renderAllNodesMarkdownWithRetry = (retryCount = 0, maxRetries = 5) => {
|
|
|
|
|
|
console.log(`🚀 renderAllNodesMarkdownWithRetry 函数被调用 (重试 ${retryCount}/${maxRetries})`);
|
|
|
|
|
|
|
|
|
|
|
|
if (!mindmapEl.value) {
|
|
|
|
|
|
console.warn('⚠️ 思维导图容器不存在,无法渲染markdown');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 查找所有节点元素
|
|
|
|
|
|
const nodeElements = mindmapEl.value.querySelectorAll('[data-id]');
|
|
|
|
|
|
console.log(`🔍 找到 ${nodeElements.length} 个节点元素`);
|
|
|
|
|
|
|
|
|
|
|
|
if (nodeElements.length === 0) {
|
|
|
|
|
|
if (retryCount < maxRetries) {
|
|
|
|
|
|
console.log(`⏳ 没有找到节点元素,${500}ms后重试...`);
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
renderAllNodesMarkdownWithRetry(retryCount + 1, maxRetries);
|
|
|
|
|
|
}, 500);
|
|
|
|
|
|
return;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.warn('⚠️ 重试次数已达上限,停止重试');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 找到节点了,开始渲染
|
|
|
|
|
|
renderAllNodesMarkdown();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染所有节点的markdown内容
|
|
|
|
|
|
const renderAllNodesMarkdown = () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log('🎨 开始渲染所有节点的markdown内容...');
|
|
|
|
|
|
console.log('📦 mindmapEl.value:', mindmapEl.value);
|
|
|
|
|
|
|
|
|
|
|
|
// 查找所有节点元素
|
|
|
|
|
|
const nodeElements = mindmapEl.value.querySelectorAll('[data-id]');
|
|
|
|
|
|
console.log(`🔍 找到 ${nodeElements.length} 个节点元素`);
|
|
|
|
|
|
|
|
|
|
|
|
nodeElements.forEach((nodeElement) => {
|
|
|
|
|
|
const nodeId = nodeElement.getAttribute('data-id');
|
|
|
|
|
|
if (!nodeId) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 查找节点文本元素
|
|
|
|
|
|
const textElement = nodeElement.querySelector('.topic-text');
|
|
|
|
|
|
if (textElement) {
|
|
|
|
|
|
const originalText = textElement.textContent || textElement.innerText;
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否包含markdown语法或表格字符
|
|
|
|
|
|
if (hasMarkdownSyntax(originalText) || originalText.includes('|')) {
|
|
|
|
|
|
console.log(`🎨 渲染节点 ${nodeId} 的markdown内容`);
|
|
|
|
|
|
console.log(`📝 原始内容:`, originalText);
|
|
|
|
|
|
|
|
|
|
|
|
// 特别检查表格
|
|
|
|
|
|
if (originalText.includes('|')) {
|
|
|
|
|
|
console.log(`📊 检测到表格内容:`, originalText);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染markdown内容
|
|
|
|
|
|
const renderedContent = smartRenderNodeContent(originalText);
|
|
|
|
|
|
console.log(`🎨 渲染后内容:`, renderedContent);
|
|
|
|
|
|
|
|
|
|
|
|
// 更新节点内容
|
|
|
|
|
|
textElement.innerHTML = renderedContent;
|
|
|
|
|
|
|
|
|
|
|
|
// 添加markdown样式类
|
|
|
|
|
|
textElement.classList.add('markdown-content');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(`📝 节点 ${nodeId} 不包含markdown语法,保持原始内容`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
console.log('✅ 所有节点的markdown内容渲染完成');
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('❌ 渲染节点markdown内容失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-04 05:47:42 +00:00
|
|
|
|
// 手动添加节点到DOM的函数
|
|
|
|
|
|
const manuallyAddNodeToDOM = async (newNode, parentNode) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log("🔧 开始手动添加节点到DOM:", newNode);
|
|
|
|
|
|
|
|
|
|
|
|
if (!mindmapEl.value) {
|
|
|
|
|
|
throw new Error("思维导图容器不存在");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 查找父节点元素
|
|
|
|
|
|
const parentElement = mindmapEl.value.querySelector(`[data-id="${parentNode.id}"]`);
|
|
|
|
|
|
if (!parentElement) {
|
|
|
|
|
|
throw new Error("找不到父节点元素");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 查找父节点的children容器
|
|
|
|
|
|
let childrenContainer = parentElement.querySelector('.children');
|
|
|
|
|
|
if (!childrenContainer) {
|
|
|
|
|
|
// 如果没有children容器,创建一个
|
|
|
|
|
|
childrenContainer = document.createElement('div');
|
|
|
|
|
|
childrenContainer.className = 'children';
|
|
|
|
|
|
parentElement.appendChild(childrenContainer);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建新节点元素
|
|
|
|
|
|
const newNodeElement = document.createElement('div');
|
|
|
|
|
|
newNodeElement.className = 'topic';
|
|
|
|
|
|
newNodeElement.setAttribute('data-id', newNode.id);
|
2025-09-10 07:27:37 +00:00
|
|
|
|
|
|
|
|
|
|
// 使用markdown渲染器渲染节点内容
|
|
|
|
|
|
const renderedTopic = smartRenderNodeContent(newNode.topic);
|
|
|
|
|
|
const renderedDescription = newNode.data?.des ? smartRenderNodeContent(newNode.data.des) : '';
|
|
|
|
|
|
|
2025-09-04 05:47:42 +00:00
|
|
|
|
newNodeElement.innerHTML = `
|
2025-09-10 07:27:37 +00:00
|
|
|
|
<div class="topic-text">${renderedTopic}</div>
|
|
|
|
|
|
${renderedDescription ? `<div class="topic-description">${renderedDescription}</div>` : ''}
|
2025-09-04 05:47:42 +00:00
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// 添加到children容器
|
|
|
|
|
|
childrenContainer.appendChild(newNodeElement);
|
|
|
|
|
|
|
|
|
|
|
|
console.log("✅ 节点已手动添加到DOM");
|
|
|
|
|
|
|
|
|
|
|
|
// 触发重新布局
|
|
|
|
|
|
if (mindElixir.value && mindElixir.value.layout) {
|
|
|
|
|
|
mindElixir.value.layout();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("手动添加节点到DOM失败:", error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 暴露方法给父组件
|
|
|
|
|
|
defineExpose({
|
2025-09-08 10:20:48 +00:00
|
|
|
|
showMindMapPage,
|
2025-09-04 05:47:42 +00:00
|
|
|
|
cleanupIntervals
|
|
|
|
|
|
});
|
2025-09-08 10:20:48 +00:00
|
|
|
|
|
|
|
|
|
|
// 实时更新思维导图(用于流式生成)
|
|
|
|
|
|
const updateMindMapRealtime = async (data, title) => {
|
|
|
|
|
|
try {
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log('🔄 开始实时更新思维导图:', title);
|
|
|
|
|
|
// console.log('🔄 接收到的数据:', data);
|
|
|
|
|
|
// console.log('🔄 数据块数量:', data.chunkCount || '未知');
|
2025-09-08 10:20:48 +00:00
|
|
|
|
|
|
|
|
|
|
// 如果还没有思维导图实例,先创建一个临时的
|
|
|
|
|
|
if (!mindElixir.value) {
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log('🆕 创建新的思维导图实例用于实时更新');
|
2025-09-08 10:20:48 +00:00
|
|
|
|
|
|
|
|
|
|
// 隐藏欢迎页面
|
|
|
|
|
|
hideWelcomePage();
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log('✅ 欢迎页面已隐藏');
|
2025-09-08 10:20:48 +00:00
|
|
|
|
|
|
|
|
|
|
// 等待Vue响应式更新完成
|
|
|
|
|
|
await nextTick();
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log('✅ Vue响应式更新完成');
|
2025-09-08 10:20:48 +00:00
|
|
|
|
|
|
|
|
|
|
// 确保思维导图容器已经准备好
|
|
|
|
|
|
let retryCount = 0;
|
|
|
|
|
|
while (!mindmapEl.value && retryCount < 20) {
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log(`⏳ 等待思维导图容器准备... (${retryCount + 1}/20)`);
|
2025-09-08 10:20:48 +00:00
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
|
|
|
|
await nextTick();
|
|
|
|
|
|
retryCount++;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!mindmapEl.value) {
|
|
|
|
|
|
console.error('❌ 思维导图容器仍未准备好,跳过此次更新');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log('🎯 开始创建Mind Elixir实例...');
|
2025-09-08 10:20:48 +00:00
|
|
|
|
mindElixir.value = new MindElixir({
|
|
|
|
|
|
el: mindmapEl.value,
|
|
|
|
|
|
direction: MindElixir.RIGHT,
|
|
|
|
|
|
draggable: true,
|
2025-10-09 06:20:51 +00:00
|
|
|
|
contextMenu: false,
|
2025-09-08 10:20:48 +00:00
|
|
|
|
toolBar: true,
|
2025-10-09 06:20:51 +00:00
|
|
|
|
nodeMenu: true,
|
|
|
|
|
|
keypress: false, // 禁用键盘快捷键,防止不入库的添加节点操作
|
2025-09-08 10:20:48 +00:00
|
|
|
|
autoCenter: false,
|
|
|
|
|
|
infinite: true,
|
|
|
|
|
|
maxScale: 5,
|
2025-09-10 07:27:37 +00:00
|
|
|
|
minScale: 0.1,
|
|
|
|
|
|
markdown: (text, nodeObj) => {
|
2025-10-09 08:02:23 +00:00
|
|
|
|
// 检查内容是否包含markdown语法(包括图片和数学公式)
|
|
|
|
|
|
if (text.includes('|') || text.includes('**') || text.includes('`') || text.includes('#') || text.includes('$') || text.includes('![')) {
|
2025-09-10 07:27:37 +00:00
|
|
|
|
const result = smartRenderNodeContent(text);
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
return text;
|
|
|
|
|
|
}
|
2025-09-08 10:20:48 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 修复:将数据包装成MindElixir期望的格式
|
2025-09-10 05:02:45 +00:00
|
|
|
|
const tempId = `temp-${Date.now()}`;
|
2025-09-08 10:20:48 +00:00
|
|
|
|
const mindElixirData = {
|
|
|
|
|
|
nodeData: data, // 将节点数据放在nodeData字段中
|
2025-09-10 05:02:45 +00:00
|
|
|
|
mindmapId: tempId, // 临时ID
|
|
|
|
|
|
id: tempId, // 同时设置id字段
|
2025-09-08 10:20:48 +00:00
|
|
|
|
title: title || 'AI生成中...'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-10 05:02:45 +00:00
|
|
|
|
// 设置当前思维导图ID为临时ID
|
|
|
|
|
|
currentMindmapId.value = tempId;
|
|
|
|
|
|
console.log('🆔 设置临时思维导图ID:', tempId);
|
|
|
|
|
|
|
2025-09-08 10:20:48 +00:00
|
|
|
|
// 初始化数据
|
|
|
|
|
|
const result = mindElixir.value.init(mindElixirData);
|
|
|
|
|
|
console.log('✅ 实时思维导图实例创建成功');
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log('✅ 初始化结果:', result);
|
2025-09-08 10:20:48 +00:00
|
|
|
|
|
|
|
|
|
|
// 绑定事件监听器
|
|
|
|
|
|
bindEventListeners();
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log('✅ 事件监听器已绑定');
|
2025-09-08 10:20:48 +00:00
|
|
|
|
|
|
|
|
|
|
// 居中显示
|
|
|
|
|
|
centerMindMap();
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log('✅ 思维导图已居中显示');
|
|
|
|
|
|
|
2025-10-10 05:04:03 +00:00
|
|
|
|
// 注意:导出按钮已由MindElixir原生工具栏提供,无需手动添加
|
|
|
|
|
|
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// 使用Mind Elixir原生markdown支持,无需手动渲染
|
2025-09-08 10:20:48 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
// 如果已有实例,直接更新数据
|
|
|
|
|
|
console.log('🔄 更新现有思维导图数据');
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 保存当前位置
|
|
|
|
|
|
const currentPosition = saveCurrentPosition();
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log(' 当前位置已保存:', currentPosition);
|
2025-09-08 10:20:48 +00:00
|
|
|
|
|
|
|
|
|
|
// 🔧 修复:将数据包装成MindElixir期望的格式
|
2025-09-10 05:02:45 +00:00
|
|
|
|
const currentId = String(currentMindmapId.value || '');
|
|
|
|
|
|
const tempId = currentId && currentId.startsWith('temp-')
|
|
|
|
|
|
? currentId
|
|
|
|
|
|
: `temp-${Date.now()}`;
|
|
|
|
|
|
|
2025-09-08 10:20:48 +00:00
|
|
|
|
const mindElixirData = {
|
|
|
|
|
|
nodeData: data, // 将节点数据放在nodeData字段中
|
2025-09-10 05:02:45 +00:00
|
|
|
|
mindmapId: tempId,
|
|
|
|
|
|
id: tempId,
|
2025-09-08 10:20:48 +00:00
|
|
|
|
title: title || 'AI生成中...'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-10 05:02:45 +00:00
|
|
|
|
// 确保当前思维导图ID是临时ID
|
|
|
|
|
|
if (!currentId || !currentId.startsWith('temp-')) {
|
|
|
|
|
|
currentMindmapId.value = tempId;
|
|
|
|
|
|
console.log('🆔 更新临时思维导图ID:', tempId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-08 10:20:48 +00:00
|
|
|
|
// 重新初始化数据
|
|
|
|
|
|
const result = mindElixir.value.init(mindElixirData);
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log('✅ 思维导图数据更新成功');
|
|
|
|
|
|
// console.log('✅ 更新结果:', result);
|
2025-09-08 10:20:48 +00:00
|
|
|
|
|
|
|
|
|
|
// 恢复位置
|
|
|
|
|
|
if (currentPosition) {
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
restorePosition(currentPosition);
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// console.log(' 位置已恢复');
|
2025-09-08 10:20:48 +00:00
|
|
|
|
}, 100);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-10 07:27:37 +00:00
|
|
|
|
// 使用Mind Elixir原生markdown支持,无需手动渲染
|
|
|
|
|
|
|
2025-09-08 10:20:48 +00:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('❌ 更新思维导图数据失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('❌ 实时更新思维导图失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-09-04 05:47:42 +00:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.mindmap-container {
|
|
|
|
|
|
height: 100vh;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
|
|
|
|
overflow: visible;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 保存控制按钮样式 */
|
|
|
|
|
|
.save-controls {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
bottom: 20px;
|
|
|
|
|
|
right: 20px;
|
|
|
|
|
|
z-index: 1000;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.save-btn {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
padding: 12px 20px;
|
|
|
|
|
|
background: #660874;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
box-shadow: 0 4px 12px rgba(102, 8, 116, 0.3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.save-btn:hover {
|
|
|
|
|
|
background: #5a0666;
|
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
|
box-shadow: 0 6px 16px rgba(102, 8, 116, 0.4);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.save-btn:active {
|
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(102, 8, 116, 0.3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.save-btn svg {
|
|
|
|
|
|
width: 16px;
|
|
|
|
|
|
height: 16px;
|
|
|
|
|
|
stroke: currentColor;
|
|
|
|
|
|
stroke-width: 2;
|
|
|
|
|
|
fill: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 刷新按钮样式 */
|
|
|
|
|
|
.refresh-btn {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
padding: 12px 20px;
|
|
|
|
|
|
background: #28a745;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.refresh-btn:hover {
|
|
|
|
|
|
background: #218838;
|
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
|
box-shadow: 0 6px 16px rgba(40, 167, 69, 0.4);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.refresh-btn:active {
|
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(40, 167, 69, 0.3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.refresh-btn svg {
|
|
|
|
|
|
width: 16px;
|
|
|
|
|
|
height: 16px;
|
|
|
|
|
|
stroke: currentColor;
|
|
|
|
|
|
stroke-width: 2;
|
|
|
|
|
|
fill: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.mindmap-el {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
overflow: visible;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 欢迎页面样式 */
|
|
|
|
|
|
.welcome-page {
|
|
|
|
|
|
height: 100vh;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
margin-left: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
|
|
|
|
|
color: #37352f;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
z-index: 999;
|
|
|
|
|
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
|
border-left: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 当AI助手折叠时,welcome-page占满全屏 */
|
|
|
|
|
|
.welcome-page.ai-sidebar-collapsed {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
margin-left: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.welcome-content {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
max-width: 800px;
|
|
|
|
|
|
padding: 40px;
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
|
border: 2px solid #660874;
|
|
|
|
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
z-index: 1001;
|
|
|
|
|
|
margin-left: 100px;
|
|
|
|
|
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 当AI助手折叠时,welcome-content居中 */
|
|
|
|
|
|
.welcome-content.ai-sidebar-collapsed {
|
|
|
|
|
|
margin-left: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.welcome-header h1 {
|
|
|
|
|
|
font-size: 3rem;
|
|
|
|
|
|
margin: 0 0 20px 0;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: #37352f;
|
|
|
|
|
|
text-shadow: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.welcome-subtitle {
|
|
|
|
|
|
font-size: 1.2rem;
|
|
|
|
|
|
margin: 0 0 40px 0;
|
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
|
font-weight: 300;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.welcome-features {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 30px;
|
|
|
|
|
|
margin: 40px 0;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.feature-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.8);
|
|
|
|
|
|
border-radius: 15px;
|
|
|
|
|
|
border: 1px solid rgba(102, 8, 116, 0.1);
|
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
max-width: 500px;
|
|
|
|
|
|
justify-content: flex-start;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.feature-item:hover {
|
|
|
|
|
|
transform: translateY(-5px);
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.95);
|
|
|
|
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.feature-icon {
|
|
|
|
|
|
font-size: 2.5rem;
|
|
|
|
|
|
min-width: 60px;
|
|
|
|
|
|
color: #660874;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.feature-text {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.feature-text h3 {
|
|
|
|
|
|
margin: 0 0 10px 0;
|
|
|
|
|
|
font-size: 1.3rem;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #37352f;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.feature-text p {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.welcome-tips {
|
|
|
|
|
|
margin-top: 30px;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.8);
|
|
|
|
|
|
border-radius: 15px;
|
|
|
|
|
|
border: 1px solid rgba(102, 8, 116, 0.1);
|
|
|
|
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.welcome-tips p {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
|
font-size: 1rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Mind Elixir 全局样式 - 将节点改为框样式 */
|
|
|
|
|
|
:deep(.mind-elixir) {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.mind-elixir .map-container) {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.mind-elixir .map-canvas) {
|
|
|
|
|
|
transition: none; /* 移除过渡动画,使拖动更流畅 */
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 节点框样式 */
|
|
|
|
|
|
:deep(.map-container .topic) {
|
|
|
|
|
|
background: #ffffff !important;
|
|
|
|
|
|
border: 2px solid #e0e0e0 !important;
|
|
|
|
|
|
border-radius: 8px !important;
|
|
|
|
|
|
padding: 12px 16px !important;
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
|
|
|
|
|
|
transition: all 0.3s ease !important;
|
|
|
|
|
|
cursor: pointer !important;
|
|
|
|
|
|
min-width: 120px !important;
|
|
|
|
|
|
max-width: 300px !important;
|
|
|
|
|
|
text-align: center !important;
|
|
|
|
|
|
font-size: 14px !important;
|
|
|
|
|
|
font-weight: 500 !important;
|
|
|
|
|
|
color: #333 !important;
|
|
|
|
|
|
position: relative !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.map-container .topic:hover) {
|
|
|
|
|
|
border-color: #007bff !important;
|
|
|
|
|
|
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.2) !important;
|
|
|
|
|
|
transform: translateY(-2px) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.map-container .topic.selected) {
|
|
|
|
|
|
border-color: #007bff !important;
|
|
|
|
|
|
background: #f8f9ff !important;
|
|
|
|
|
|
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 悬停菜单样式 */
|
|
|
|
|
|
.context-menu {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.95);
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
padding: 8px;
|
|
|
|
|
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
|
|
|
|
|
backdrop-filter: blur(10px);
|
|
|
|
|
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
|
|
|
|
z-index: 1000;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: row;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
min-width: auto;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
animation: menuFadeIn 0.2s ease;
|
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes menuFadeIn {
|
|
|
|
|
|
from {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: translateX(-50%) translateY(-10px);
|
|
|
|
|
|
}
|
|
|
|
|
|
to {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
transform: translateX(-50%) translateY(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.context-menu-item {
|
|
|
|
|
|
width: 36px;
|
|
|
|
|
|
height: 36px;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.context-menu-item:hover {
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.05);
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
transform: scale(1.05);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.context-menu-item.delete:hover {
|
|
|
|
|
|
background: rgba(220, 53, 69, 0.1);
|
|
|
|
|
|
color: #dc3545;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-08 07:06:08 +00:00
|
|
|
|
/* Ask AI 特殊样式 - 让它一眼就突出 */
|
|
|
|
|
|
.context-menu-item.ask-ai {
|
|
|
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border: 2px solid #5a67d8;
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.context-menu-item.ask-ai:hover {
|
|
|
|
|
|
background: linear-gradient(135deg, #5a67d8 0%, #667eea 100%);
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.context-menu-item.ask-ai svg {
|
|
|
|
|
|
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-04 05:47:42 +00:00
|
|
|
|
.context-menu-item svg {
|
|
|
|
|
|
width: 16px;
|
|
|
|
|
|
height: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 工具提示样式 */
|
|
|
|
|
|
.context-menu-item::after {
|
|
|
|
|
|
content: attr(title);
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
bottom: -30px;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.8);
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
visibility: hidden;
|
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
z-index: 1001;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.context-menu-item:hover::after {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
visibility: visible;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.context-menu-item::before {
|
|
|
|
|
|
content: '';
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
bottom: -8px;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
|
border: 4px solid transparent;
|
|
|
|
|
|
border-top-color: rgba(0, 0, 0, 0.8);
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
visibility: hidden;
|
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.context-menu-item:hover::before {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
visibility: visible;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-08 07:06:08 +00:00
|
|
|
|
/* AI输入区域样式 */
|
|
|
|
|
|
.ai-input-area {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
width: 300px;
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
|
|
|
|
|
z-index: 1001;
|
|
|
|
|
|
animation: slideInDown 0.3s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes slideInDown {
|
|
|
|
|
|
from {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: translateX(-50%) translateY(-10px);
|
|
|
|
|
|
}
|
|
|
|
|
|
to {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
transform: translateX(-50%) translateY(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-input-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
|
border-bottom: 1px solid #e9ecef;
|
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
|
border-radius: 12px 12px 0 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-input-title {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #2c3e50;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-close-btn {
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
color: #6c757d;
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-close-btn:hover {
|
|
|
|
|
|
background: #e9ecef;
|
|
|
|
|
|
color: #495057;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-input-content {
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-input-content textarea {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
border: 2px solid #e9ecef;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
resize: none;
|
|
|
|
|
|
transition: border-color 0.2s ease;
|
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-input-content textarea:focus {
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
border-color: #007bff;
|
|
|
|
|
|
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-input-content textarea:disabled {
|
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
|
color: #6c757d;
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-input-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-cancel, .btn-submit {
|
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-cancel {
|
|
|
|
|
|
background: #6c757d;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-cancel:hover:not(:disabled) {
|
|
|
|
|
|
background: #5a6268;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-submit {
|
|
|
|
|
|
background: #007bff;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-submit:hover:not(:disabled) {
|
|
|
|
|
|
background: #0056b3;
|
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-cancel:disabled, .btn-submit:disabled {
|
|
|
|
|
|
opacity: 0.6;
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-04 05:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* 响应式设计 */
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
|
.context-menu {
|
|
|
|
|
|
padding: 6px;
|
|
|
|
|
|
gap: 3px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.context-menu-item {
|
|
|
|
|
|
width: 32px;
|
|
|
|
|
|
height: 32px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.context-menu-item svg {
|
|
|
|
|
|
width: 14px;
|
|
|
|
|
|
height: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 节点描述显示样式 */
|
|
|
|
|
|
.node-description {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
margin-top: 6px;
|
|
|
|
|
|
padding: 6px 8px;
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.03);
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
max-width: 250px;
|
|
|
|
|
|
word-wrap: break-word;
|
|
|
|
|
|
line-height: 1.3;
|
|
|
|
|
|
border-left: 3px solid #e0e0e0;
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 节点标题样式调整 - 统一所有节点为框样式 */
|
|
|
|
|
|
.topic {
|
|
|
|
|
|
min-width: 150px;
|
|
|
|
|
|
max-width: 400px;
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
background: white !important;
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
|
|
|
|
|
|
border: 1px solid #e0e0e0 !important;
|
|
|
|
|
|
margin: 4px !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 确保所有层级的节点都使用框样式 */
|
|
|
|
|
|
.topic.root {
|
|
|
|
|
|
background: white !important;
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
|
|
|
|
|
|
border: 1px solid #e0e0e0 !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.topic.main {
|
|
|
|
|
|
background: white !important;
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
|
|
|
|
|
|
border: 1px solid #e0e0e0 !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.topic.sub {
|
|
|
|
|
|
background: white !important;
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
|
|
|
|
|
|
border: 1px solid #e0e0e0 !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 强制所有节点都显示为框样式 */
|
|
|
|
|
|
.topic {
|
|
|
|
|
|
background: white !important;
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
|
|
|
|
|
|
border: 1px solid #e0e0e0 !important;
|
|
|
|
|
|
border-radius: 8px !important;
|
|
|
|
|
|
padding: 8px 12px !important;
|
|
|
|
|
|
min-width: 150px !important;
|
|
|
|
|
|
max-width: 400px !important;
|
|
|
|
|
|
margin: 4px !important;
|
|
|
|
|
|
position: relative !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 调整连接线位置 */
|
|
|
|
|
|
.tpc-line {
|
|
|
|
|
|
position: absolute !important;
|
|
|
|
|
|
z-index: 1 !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 确保连接线正确连接到节点中心 */
|
|
|
|
|
|
.tpc-line.tpc-line-left {
|
|
|
|
|
|
right: 100% !important;
|
|
|
|
|
|
top: 50% !important;
|
|
|
|
|
|
transform: translateY(-50%) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tpc-line.tpc-line-right {
|
|
|
|
|
|
left: 100% !important;
|
|
|
|
|
|
top: 50% !important;
|
|
|
|
|
|
transform: translateY(-50%) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tpc-line.tpc-line-top {
|
|
|
|
|
|
bottom: 100% !important;
|
|
|
|
|
|
left: 50% !important;
|
|
|
|
|
|
transform: translateX(-50%) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tpc-line.tpc-line-bottom {
|
|
|
|
|
|
top: 100% !important;
|
|
|
|
|
|
left: 50% !important;
|
|
|
|
|
|
transform: translateX(-50%) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 移除MindElixir默认的圆形样式和线样式 */
|
|
|
|
|
|
.topic::before {
|
|
|
|
|
|
display: none !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.topic::after {
|
|
|
|
|
|
display: none !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 移除所有可能的线样式 */
|
|
|
|
|
|
.topic[style*="border-radius: 50%"] {
|
|
|
|
|
|
border-radius: 8px !important;
|
|
|
|
|
|
background: white !important;
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
|
|
|
|
|
|
border: 1px solid #e0e0e0 !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 确保没有子节点的节点也显示为框 */
|
|
|
|
|
|
.topic:not(:has(.children)) {
|
|
|
|
|
|
background: white !important;
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
|
|
|
|
|
|
border: 1px solid #e0e0e0 !important;
|
|
|
|
|
|
border-radius: 8px !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 强制覆盖所有内联样式 */
|
|
|
|
|
|
.topic[style] {
|
|
|
|
|
|
background: white !important;
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
|
|
|
|
|
|
border: 1px solid #e0e0e0 !important;
|
|
|
|
|
|
border-radius: 8px !important;
|
|
|
|
|
|
padding: 8px 12px !important;
|
|
|
|
|
|
min-width: 150px !important;
|
|
|
|
|
|
max-width: 400px !important;
|
|
|
|
|
|
margin: 4px !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 针对me-tpc元素(MindElixir的节点元素)强制应用框样式 */
|
|
|
|
|
|
/* 移除强制样式,让MindElixir使用原始样式 */
|
|
|
|
|
|
|
|
|
|
|
|
/* 保持基本的文字样式,但不强制覆盖MindElixir样式 */
|
|
|
|
|
|
.topic .topic-text {
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-10 07:27:37 +00:00
|
|
|
|
/* Markdown内容样式 */
|
|
|
|
|
|
.topic .topic-text.markdown-content {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
line-height: 1.3;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-10 10:26:48 +00:00
|
|
|
|
/* 强制表格样式 - 最高优先级 */
|
|
|
|
|
|
.topic table,
|
|
|
|
|
|
.topic .text table,
|
|
|
|
|
|
.topic .topic-text table,
|
2025-09-10 07:27:37 +00:00
|
|
|
|
.topic .topic-text.markdown-content table {
|
|
|
|
|
|
border-collapse: collapse !important;
|
|
|
|
|
|
width: 100% !important;
|
|
|
|
|
|
margin: 4px 0 !important;
|
|
|
|
|
|
font-size: 11px !important;
|
2025-09-10 10:26:48 +00:00
|
|
|
|
border: 2px solid #333 !important;
|
|
|
|
|
|
border-radius: 6px !important;
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08) !important;
|
|
|
|
|
|
background-color: #fafafa !important;
|
|
|
|
|
|
overflow: hidden !important;
|
2025-09-10 07:27:37 +00:00
|
|
|
|
display: table !important;
|
2025-09-10 10:26:48 +00:00
|
|
|
|
white-space: normal !important; /* 覆盖MindElixir的pre-wrap */
|
2025-09-10 07:27:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-10 10:26:48 +00:00
|
|
|
|
.topic table th,
|
|
|
|
|
|
.topic table td,
|
|
|
|
|
|
.topic .text table th,
|
|
|
|
|
|
.topic .text table td,
|
|
|
|
|
|
.topic .topic-text table th,
|
|
|
|
|
|
.topic .topic-text table td,
|
2025-09-10 07:27:37 +00:00
|
|
|
|
.topic .topic-text.markdown-content table th,
|
|
|
|
|
|
.topic .topic-text.markdown-content table td {
|
2025-09-10 10:26:48 +00:00
|
|
|
|
border: 2px solid #333 !important;
|
|
|
|
|
|
padding: 6px 8px !important;
|
2025-09-10 07:27:37 +00:00
|
|
|
|
text-align: left !important;
|
|
|
|
|
|
vertical-align: top !important;
|
|
|
|
|
|
display: table-cell !important;
|
2025-09-10 10:26:48 +00:00
|
|
|
|
position: relative !important;
|
|
|
|
|
|
white-space: normal !important; /* 覆盖MindElixir的pre-wrap */
|
2025-09-10 07:27:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-10 10:26:48 +00:00
|
|
|
|
.topic .topic-text.markdown-content table th,
|
|
|
|
|
|
.topic .text table th,
|
|
|
|
|
|
.topic table th {
|
2025-09-10 07:27:37 +00:00
|
|
|
|
background-color: #f5f5f5 !important;
|
|
|
|
|
|
font-weight: 600 !important;
|
2025-09-10 10:26:48 +00:00
|
|
|
|
color: #333 !important;
|
|
|
|
|
|
text-align: center !important;
|
|
|
|
|
|
border-bottom: 2px solid #333 !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.topic .topic-text.markdown-content table td,
|
|
|
|
|
|
.topic .text table td,
|
|
|
|
|
|
.topic table td {
|
|
|
|
|
|
background-color: #fff !important;
|
2025-09-10 07:27:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-10 10:26:48 +00:00
|
|
|
|
.topic .topic-text.markdown-content table tr,
|
|
|
|
|
|
.topic .text table tr,
|
|
|
|
|
|
.topic table tr {
|
2025-09-10 07:27:37 +00:00
|
|
|
|
display: table-row !important;
|
2025-09-10 10:26:48 +00:00
|
|
|
|
white-space: normal !important; /* 覆盖MindElixir的pre-wrap */
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.topic .topic-text.markdown-content table tr:nth-child(even) td,
|
|
|
|
|
|
.topic .text table tr:nth-child(even) td {
|
|
|
|
|
|
background-color: #f8f8f8 !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.topic .topic-text.markdown-content table tr:hover td,
|
|
|
|
|
|
.topic .text table tr:hover td {
|
|
|
|
|
|
background-color: #f0f8ff !important;
|
2025-09-10 07:27:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-10 10:26:48 +00:00
|
|
|
|
/* 简洁的边框效果 */
|
|
|
|
|
|
.topic .topic-text.markdown-content table th:not(:last-child),
|
|
|
|
|
|
.topic .topic-text.markdown-content table td:not(:last-child),
|
|
|
|
|
|
.topic .text table th:not(:last-child),
|
|
|
|
|
|
.topic .text table td:not(:last-child) {
|
|
|
|
|
|
border-right: 1px solid #e0e0e0 !important;
|
2025-09-10 07:27:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-10 10:26:48 +00:00
|
|
|
|
.topic .topic-text.markdown-content table tr:not(:last-child) td,
|
|
|
|
|
|
.topic .text table tr:not(:last-child) td {
|
|
|
|
|
|
border-bottom: 1px solid #e0e0e0 !important;
|
2025-09-10 07:27:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Markdown代码样式 */
|
|
|
|
|
|
.topic .topic-text.markdown-content code {
|
|
|
|
|
|
background-color: #f4f4f4;
|
|
|
|
|
|
padding: 1px 3px;
|
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
font-family: 'Courier New', monospace;
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.topic .topic-text.markdown-content pre {
|
|
|
|
|
|
background-color: #f4f4f4;
|
|
|
|
|
|
padding: 4px;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Markdown列表样式 */
|
|
|
|
|
|
.topic .topic-text.markdown-content ul,
|
|
|
|
|
|
.topic .topic-text.markdown-content ol {
|
|
|
|
|
|
margin: 2px 0;
|
|
|
|
|
|
padding-left: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.topic .topic-text.markdown-content li {
|
|
|
|
|
|
margin: 1px 0;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Markdown强调样式 */
|
|
|
|
|
|
.topic .topic-text.markdown-content strong {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.topic .topic-text.markdown-content em {
|
|
|
|
|
|
font-style: italic;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-04 05:47:42 +00:00
|
|
|
|
/* HTML内容样式 */
|
|
|
|
|
|
.topic h1, .topic h2, .topic h3, .topic h4, .topic h5, .topic h6 {
|
|
|
|
|
|
margin: 4px 0;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.topic p {
|
|
|
|
|
|
margin: 2px 0;
|
|
|
|
|
|
line-height: 1.3;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.topic ul, .topic ol {
|
|
|
|
|
|
margin: 2px 0;
|
|
|
|
|
|
padding-left: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.topic li {
|
|
|
|
|
|
margin: 1px 0;
|
|
|
|
|
|
line-height: 1.3;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.topic strong, .topic b {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.topic em, .topic i {
|
|
|
|
|
|
font-style: italic;
|
|
|
|
|
|
color: #555;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.topic code {
|
|
|
|
|
|
background: #f5f5f5;
|
|
|
|
|
|
padding: 1px 3px;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
font-family: monospace;
|
|
|
|
|
|
font-size: 0.9em;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 确保节点内容能够完整显示 */
|
|
|
|
|
|
.topic-content {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2025-10-10 06:40:19 +00:00
|
|
|
|
align-items: center;
|
2025-09-04 05:47:42 +00:00
|
|
|
|
}
|
2025-10-10 05:04:03 +00:00
|
|
|
|
|
|
|
|
|
|
/* 强制设置节点中图片的大小 */
|
|
|
|
|
|
.map-container me-tpc img,
|
|
|
|
|
|
.map-container me-tpc > img {
|
|
|
|
|
|
max-width: 200px !important;
|
|
|
|
|
|
max-height: 150px !important;
|
|
|
|
|
|
width: auto !important;
|
|
|
|
|
|
height: auto !important;
|
|
|
|
|
|
display: block !important;
|
|
|
|
|
|
margin-bottom: 8px !important;
|
|
|
|
|
|
object-fit: cover !important;
|
|
|
|
|
|
}
|
2025-10-10 18:39:23 +00:00
|
|
|
|
|
|
|
|
|
|
/* 图片预览模态框样式 */
|
|
|
|
|
|
.image-preview-modal {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.8);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
z-index: 10000;
|
|
|
|
|
|
animation: fadeIn 0.3s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes fadeIn {
|
|
|
|
|
|
from {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
to {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.image-preview-content {
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
max-width: 90%;
|
|
|
|
|
|
max-height: 90%;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
|
|
|
|
|
animation: slideIn 0.3s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes slideIn {
|
|
|
|
|
|
from {
|
|
|
|
|
|
transform: translateY(-20px);
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
to {
|
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.image-preview-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
|
border-bottom: 1px solid #e0e0e0;
|
|
|
|
|
|
background: #f5f5f5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.image-preview-title {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.image-preview-close {
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
font-size: 28px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
width: 32px;
|
|
|
|
|
|
height: 32px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.image-preview-close:hover {
|
|
|
|
|
|
background: #e0e0e0;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.image-preview-body {
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
min-height: 200px;
|
|
|
|
|
|
max-height: 80vh;
|
|
|
|
|
|
overflow: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.preview-image {
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
max-height: 75vh;
|
|
|
|
|
|
height: auto;
|
|
|
|
|
|
object-fit: contain;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.image-preview-loading,
|
|
|
|
|
|
.image-preview-error {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
padding: 40px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.loading-spinner {
|
|
|
|
|
|
width: 40px;
|
|
|
|
|
|
height: 40px;
|
|
|
|
|
|
border: 4px solid #f3f3f3;
|
|
|
|
|
|
border-top: 4px solid #660874;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
animation: spin 1s linear infinite;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes spin {
|
|
|
|
|
|
0% { transform: rotate(0deg); }
|
|
|
|
|
|
100% { transform: rotate(360deg); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.image-preview-error p {
|
|
|
|
|
|
margin: 8px 0;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.retry-button {
|
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
|
background: #660874;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
transition: background 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.retry-button:hover {
|
|
|
|
|
|
background: #7d0a8e;
|
|
|
|
|
|
}
|
2025-10-11 04:51:05 +00:00
|
|
|
|
|
2025-09-04 05:47:42 +00:00
|
|
|
|
</style>
|
|
|
|
|
|
|