ForcePilot/web/src/components/MindMapSection.vue
2026-01-15 06:01:34 +08:00

391 lines
8.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="mindmap-section">
<div class="section-content">
<!-- 加载状态 -->
<div v-if="loading" class="loading-state">
<a-spin size="small" />
<span>加载中...</span>
</div>
<!-- 生成中状态 -->
<div v-else-if="generating" class="generating-state">
<a-spin size="small" />
<span>AI 正在生成思维导图...</span>
</div>
<!-- 空状态 -->
<div v-else-if="!mindmapData" class="empty-state">
<Network :size="32" />
<p>暂无思维导图</p>
<a-button type="primary" size="small" @click="generateMindmap">
<template #icon><Sparkles :size="14" /></template>
生成思维导图
</a-button>
</div>
<!-- 思维导图显示 -->
<div v-else class="mindmap-container">
<div class="mindmap-toolbar">
<a-space :size="8">
<a-button
type="text"
size="small"
@click="refreshMindmap"
:loading="generating"
title="重新生成"
>
<template #icon><RefreshCw :size="14" /></template>
<span class="toolbar-text">重新生成</span>
</a-button>
<a-button type="text" size="small" @click="fitView" title="适应视图">
<template #icon><Maximize2 :size="14" /></template>
<span class="toolbar-text">适应视图</span>
</a-button>
</a-space>
</div>
<div class="mindmap-svg-container">
<svg ref="mindmapSvg" class="mindmap-svg"></svg>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
import { message } from 'ant-design-vue'
import { RefreshCw, Network, Sparkles, Maximize2 } from 'lucide-vue-next'
import { mindmapApi } from '@/apis/mindmap_api'
import { Markmap } from 'markmap-view'
import { Transformer } from 'markmap-lib'
const props = defineProps({
databaseId: {
type: String,
required: true
}
})
// ============================================================================
// 状态管理
// ============================================================================
const loading = ref(false)
const generating = ref(false)
const mindmapData = ref(null)
const mindmapSvg = ref(null)
let markmapInstance = null
// ============================================================================
// 方法
// ============================================================================
/**
* 加载思维导图
*/
const loadMindmap = async () => {
if (!props.databaseId) return
try {
loading.value = true
const response = await mindmapApi.getByDatabase(props.databaseId)
if (response.mindmap) {
mindmapData.value = response.mindmap
await nextTick()
// 延迟渲染确保DOM完全更新
setTimeout(() => {
renderMindmap(response.mindmap)
}, 100)
}
} catch (error) {
// 如果是404错误说明还没有生成静默处理
if (
error?.message?.includes('404') ||
error?.message?.includes('不存在') ||
error?.message?.includes('还没有生成')
) {
mindmapData.value = null
} else {
console.error('加载思维导图失败:', error)
const errorMsg = error?.message || String(error)
message.error('加载思维导图失败: ' + errorMsg)
}
} finally {
loading.value = false
}
}
/**
* 生成思维导图
*/
const generateMindmap = async () => {
if (!props.databaseId) return
try {
generating.value = true
const response = await mindmapApi.generateMindmap(
props.databaseId,
[], // 使用所有文件
'' // 无自定义提示
)
mindmapData.value = response.mindmap
// 等待DOM更新
await nextTick()
// 再延迟一点确保SVG元素完全渲染
setTimeout(() => {
renderMindmap(response.mindmap)
message.success('思维导图生成成功!')
}, 100)
} catch (error) {
console.error('生成思维导图失败:', error)
const errorMsg = error?.message || String(error)
message.error('生成失败: ' + errorMsg)
} finally {
generating.value = false
}
}
/**
* 刷新思维导图
*/
const refreshMindmap = async () => {
await generateMindmap()
}
/**
* 将JSON转换为Markdown
*/
const jsonToMarkdown = (node, level = 0) => {
if (!node || !node.content) return ''
const indent = '#'.repeat(level + 1)
let markdown = `${indent} ${node.content}\n\n`
if (node.children && node.children.length > 0) {
for (const child of node.children) {
markdown += jsonToMarkdown(child, level + 1)
}
}
return markdown
}
/**
* 渲染思维导图
*/
const renderMindmap = (data, retryCount = 0) => {
if (!data) return
if (!mindmapSvg.value) {
// 如果SVG引用还没准备好最多重试3次
if (retryCount < 3) {
setTimeout(() => {
renderMindmap(data, retryCount + 1)
}, 100)
return
} else {
console.error('无法获取SVG容器渲染失败')
message.error('渲染失败无法找到SVG容器')
return
}
}
try {
// 清空之前的实例
if (markmapInstance) {
markmapInstance.destroy()
}
// 将JSON转换为Markdown
const markdown = jsonToMarkdown(data)
// 使用Transformer转换
const transformer = new Transformer()
const { root } = transformer.transform(markdown)
// 创建Markmap实例
markmapInstance = Markmap.create(mindmapSvg.value, {
duration: 300,
maxWidth: 200,
nodeMinHeight: 24,
paddingX: 8,
spacingVertical: 5,
spacingHorizontal: 60
})
markmapInstance.setData(root)
markmapInstance.fit()
// 延迟再次适应,确保布局完全稳定
setTimeout(() => {
if (markmapInstance) {
markmapInstance.fit()
}
}, 300)
} catch (error) {
console.error('渲染思维导图失败:', error)
message.error('渲染失败: ' + error.message)
}
}
/**
* 适应视图
*/
const fitView = () => {
if (markmapInstance) {
markmapInstance.fit()
}
}
/**
* 暴露给父组件的方法
*/
defineExpose({
refreshMindmap,
generateMindmap
})
// ============================================================================
// 生命周期
// ============================================================================
// 监听数据库ID变化
watch(
() => props.databaseId,
(newId) => {
if (newId) {
loadMindmap()
}
},
{ immediate: true }
)
// 监听容器大小变化,自动适应
let resizeObserver = null
onMounted(() => {
// 设置ResizeObserver监听容器大小变化
nextTick(() => {
if (mindmapSvg.value) {
const container = mindmapSvg.value.parentElement
if (container) {
resizeObserver = new ResizeObserver(() => {
if (markmapInstance) {
markmapInstance.fit()
}
})
resizeObserver.observe(container)
}
}
})
})
// 清理
onUnmounted(() => {
if (markmapInstance) {
markmapInstance.destroy()
}
if (resizeObserver) {
resizeObserver.disconnect()
}
})
</script>
<style scoped lang="less">
.mindmap-section {
display: flex;
flex-direction: column;
background: var(--gray-0);
border-top: 1px solid var(--border-color);
height: 100%;
min-height: 0;
}
.section-content {
flex: 1;
min-height: 200px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.loading-state,
.generating-state,
.empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
padding: 24px;
color: var(--text-secondary);
font-size: 13px;
svg {
color: var(--text-tertiary);
}
p {
margin: 0;
}
}
.mindmap-container {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
}
.mindmap-toolbar {
padding: 8px 16px;
background: var(--gray-0);
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: flex-end;
.toolbar-text {
margin-left: 4px;
font-size: 13px;
}
:deep(.ant-btn-text) {
display: flex;
align-items: center;
&:hover {
background: rgba(0, 0, 0, 0.04);
}
}
}
.mindmap-svg-container {
flex: 1;
position: relative;
overflow: hidden;
background: var(--gray-0);
}
.mindmap-svg {
width: 100%;
height: 100%;
min-height: 150px;
display: block;
}
// 确保父容器有高度
:deep(.markmap) {
width: 100% !important;
height: 100% !important;
}
</style>