Skip to content

Commit b111bb0

Browse files
authored
feat: 优化思维导图相关的展示效果,思维导图支持全屏展示 (#94)
* feat: 优化思维导图相关的展示效果,思维导图支持全屏展示 * feat: 优化部署文档,优先推荐 docker-compose 一键部署
1 parent af059d6 commit b111bb0

6 files changed

Lines changed: 419 additions & 82 deletions

File tree

Makefile

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,6 @@ help:
1111
@echo " run: Run project"
1212
@echo " stop: Stop project"
1313

14-
.PHONY: docker-image
15-
docker-image:
16-
@for module in $(MODULES) ; do \
17-
echo "[docker-image] start to build $(PROJECT_NAME)-$$module."; \
18-
cd $(ROOT_DIR)/$$module/; \
19-
docker build -t $(PROJECT_NAME)-$$module:$(IMAGE_VERSION) .; \
20-
done
21-
22-
@echo "当前服务配置如下:";
23-
@cat $(ROOT_DIR)/variables.env;
24-
@echo "📣 为确保程序正常运行,请检查:";
25-
@echo "1️⃣ 请按指引(https://github.com/hanshuaikang/AI-Media2Doc/blob/main/backend/README.md)了解如何获取上述配置项。";
26-
@echo "2️⃣ 在项目根目录的 variables.env 文件中填写相应的配置项。";
27-
@echo "3️⃣ 运行 make run 启动项目。";
28-
2914
.PHONY: run
3015
run:
3116
docker compose up -d

README.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,16 +81,15 @@ AI 视频创作助手源于我年初的一个想法, 作为一个喜欢阅读的
8181
### 📦 docker 本地一键部署
8282
0️⃣:安装 `docker`, 这个网上有教程, Windows 玩家建议使用 `wsl` 启动该项目。
8383

84-
1️⃣:镜像构建, 在项目根目录下执行:
85-
```shell
86-
$ make docker-image
87-
```
84+
1️⃣:下载项目首页的 docker-compose.yaml 文件
85+
8886
2️⃣:参考`variables_template.env` 在项目根目录下生成 `variables.env` 文件。 请根据 [后端部署指引 / 配置项说明](https://github.com/hanshuaikang/AI-Media2Doc/blob/main/backend/README.md#%E5%9C%A8%E7%81%AB%E5%B1%B1%E5%BC%95%E6%93%8E%E8%8E%B7%E5%8F%96%E5%AF%B9%E5%BA%94%E7%9A%84%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F%E7%9A%84%E5%80%BC) 完善 `variables.env` 文件中的环境变量。
87+
variables.env 必须与 docker-compose.yaml 在同一目录下, 建议建个新的文件夹单独存放这两个文件。
8988

90-
3️⃣:运行项目,在项目根目录下执行:
89+
3️⃣:运行, 在 docker-compose.yaml 所在目录下运行:
9190

9291
```shell
93-
$ make run
92+
$ docker-compose -f docker-compose.yaml up -d
9493
```
9594

9695

docker-compose.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
services:
22
backend:
3-
image: ai-media2doc-backend:latest
3+
image: hanshugithub/ai-media2doc-backend:latest
44
env_file:
55
- "variables.env"
66
deploy:
@@ -10,7 +10,7 @@ services:
1010
- "8080:8080"
1111

1212
frontend:
13-
image: ai-media2doc-frontend:latest
13+
image: hanshugithub/ai-media2doc-frontend:latest
1414
deploy:
1515
mode: replicated
1616
replicas: 1

frontend/src/components/VideoToMarkdown/GeneratedContentPanel.vue

Lines changed: 4 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,7 @@
77
</div>
88
<div class="original-text-content markdown-content-area">
99
<template v-if="isContentMindMap">
10-
<div id="mindMapContainer" class="mind-map-container"></div>
11-
<div class="mindmap-tip">
12-
点击下载思维导图, 导入到 <a href="https://wanglin2.github.io/mind-map/#/"
13-
target="_blank">https://wanglin2.github.io/mind-map/#/</a> 即可在线编辑
14-
</div>
10+
<MindMapViewer :content="content" />
1511
</template>
1612
<template v-else>
1713
<div v-html="renderedContent" class="markdown-content" />
@@ -21,11 +17,11 @@
2117
</template>
2218

2319
<script setup>
24-
import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
25-
import { ElButton, ElMessage } from 'element-plus'
20+
import { computed } from 'vue'
21+
import { ElButton } from 'element-plus'
2622
import { Download } from '@element-plus/icons-vue'
2723
import MarkdownIt from 'markdown-it'
28-
import MindMap from 'simple-mind-map'
24+
import MindMapViewer from './MindMapViewer.vue'
2925
3026
const props = defineProps({
3127
content: {
@@ -48,8 +44,6 @@ const md = new MarkdownIt({
4844
// 启用表格插件
4945
md.enable('table')
5046
51-
const mindMapInstance = ref(null)
52-
5347
// 判断内容是否为JSON格式
5448
const isJsonString = (str) => {
5549
if (typeof str !== 'string') return false
@@ -75,50 +69,6 @@ const renderedContent = computed(() => {
7569
return md.render(props.content)
7670
})
7771
78-
// 转换思维导图数据格式
79-
const convertToMindMapFormat = (jsonData) => {
80-
try {
81-
const data = typeof jsonData === 'object' ? jsonData : JSON.parse(jsonData)
82-
return data.data && (data.data.text || data.data.title)
83-
? data
84-
: { data: { text: data.text || data.title || "思维导图" }, children: data.children || [] }
85-
} catch {
86-
return { data: { text: "解析失败的思维导图" }, children: [] }
87-
}
88-
}
89-
90-
// 初始化思维导图
91-
const initMindMap = async () => {
92-
try {
93-
if (mindMapInstance.value) mindMapInstance.value.destroy()
94-
await nextTick()
95-
const container = document.getElementById('mindMapContainer')
96-
if (!container) return
97-
container.style.width = '100%'
98-
container.style.height = '500px'
99-
const mindMapData = convertToMindMapFormat(props.content)
100-
mindMapInstance.value = new MindMap({
101-
el: container,
102-
data: mindMapData,
103-
theme: 'primary',
104-
layout: 'mindMap',
105-
enableNodeDragging: false,
106-
height: 500,
107-
width: container.clientWidth,
108-
keypress: false,
109-
contextMenu: false,
110-
fit: true,
111-
scale: 0.8,
112-
textAutoWrap: true,
113-
nodeTextEdit: false
114-
})
115-
mindMapInstance.value.render()
116-
setTimeout(() => mindMapInstance.value?.command?.executeCommand('fit'), 300)
117-
} catch {
118-
ElMessage.error('思维导图初始化失败')
119-
}
120-
}
121-
12272
// 下载内容
12373
const downloadContent = () => {
12474
let filename, type
@@ -140,11 +90,6 @@ const downloadContent = () => {
14090
URL.revokeObjectURL(url)
14191
document.body.removeChild(a)
14292
}
143-
144-
// 组件生命周期
145-
onMounted(() => isContentMindMap.value && initMindMap())
146-
onBeforeUnmount(() => mindMapInstance.value?.destroy())
147-
watch(() => props.content, () => isContentMindMap.value && initMindMap())
14893
</script>
14994

15095
<style scoped>
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
<template>
2+
<div class="mindmap-fullscreen">
3+
<div id="mindMapContainerFull" ref="containerRef" class="mind-map-container-full"></div>
4+
5+
<div class="fullscreen-toolbar">
6+
<el-button type="primary" :icon="Close" circle size="small" title="退出全屏" @click="handleClose" />
7+
<div class="toolbar-divider"></div>
8+
<el-button type="default" :icon="ZoomIn" circle size="small" title="放大" @click="handleZoomIn" />
9+
<el-button type="default" :icon="ZoomOut" circle size="small" title="缩小" @click="handleZoomOut" />
10+
<el-button type="default" :icon="Aim" circle size="small" title="适应画布" @click="handleFit" />
11+
</div>
12+
</div>
13+
</template>
14+
15+
<script setup>
16+
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
17+
import { ElMessage, ElButton } from 'element-plus'
18+
import { Close, ZoomIn, ZoomOut, Aim } from '@element-plus/icons-vue'
19+
import MindMap from 'simple-mind-map'
20+
21+
const props = defineProps({
22+
content: { type: String, required: true }
23+
})
24+
25+
const emit = defineEmits(['close'])
26+
27+
const containerRef = ref(null)
28+
const mindMapInstance = ref(null)
29+
30+
const convertToMindMapFormat = (jsonData) => {
31+
try {
32+
const data = typeof jsonData === 'object' ? jsonData : JSON.parse(jsonData)
33+
return data.data && (data.data.text || data.data.title)
34+
? data
35+
: { data: { text: data.text || data.title || "思维导图" }, children: data.children || [] }
36+
} catch {
37+
return { data: { text: "解析失败的思维导图" }, children: [] }
38+
}
39+
}
40+
41+
const adjustView = () => {
42+
const mm = mindMapInstance.value
43+
if (!mm) return
44+
mm.command?.executeCommand('fit')
45+
requestAnimationFrame(() => {
46+
if (mm.view?.translateToCenter) {
47+
mm.view.translateToCenter()
48+
} else {
49+
mm.command?.executeCommand('TO_CENTER')
50+
}
51+
})
52+
}
53+
54+
const initMindMap = async () => {
55+
try {
56+
if (!containerRef.value) return
57+
if (mindMapInstance.value) mindMapInstance.value.destroy()
58+
await nextTick()
59+
60+
const container = containerRef.value
61+
const mindMapData = convertToMindMapFormat(props.content)
62+
63+
mindMapInstance.value = new MindMap({
64+
el: container,
65+
data: mindMapData,
66+
theme: 'primary',
67+
layout: 'logicalStructure',
68+
enableNodeDragging: true,
69+
height: window.innerHeight,
70+
width: window.innerWidth,
71+
keypress: true,
72+
contextMenu: false,
73+
fit: true,
74+
scale: 0.9,
75+
textAutoWrap: true,
76+
nodeTextEdit: false,
77+
mousewheelAction: 'zoom',
78+
enableMouseWheel: true
79+
})
80+
81+
mindMapInstance.value.render()
82+
setTimeout(() => adjustView(), 120)
83+
} catch {
84+
ElMessage.error('思维导图初始化失败')
85+
}
86+
}
87+
88+
const handleZoomIn = () => {
89+
mindMapInstance.value?.view?.enlarge()
90+
}
91+
92+
const handleZoomOut = () => {
93+
mindMapInstance.value?.view?.narrow()
94+
}
95+
96+
const handleFit = () => {
97+
adjustView()
98+
}
99+
100+
const handleClose = () => {
101+
emit('close')
102+
}
103+
104+
let resizeRaf = 0
105+
const handleResize = () => {
106+
cancelAnimationFrame(resizeRaf)
107+
resizeRaf = requestAnimationFrame(() => {
108+
if (mindMapInstance.value) {
109+
mindMapInstance.value.resize()
110+
adjustView()
111+
}
112+
})
113+
}
114+
115+
const handleKeydown = (e) => {
116+
if (e.key === 'Escape') {
117+
handleClose()
118+
}
119+
}
120+
121+
onMounted(() => {
122+
initMindMap()
123+
window.addEventListener('keydown', handleKeydown)
124+
window.addEventListener('resize', handleResize)
125+
})
126+
127+
onBeforeUnmount(() => {
128+
mindMapInstance.value?.destroy()
129+
window.removeEventListener('keydown', handleKeydown)
130+
window.removeEventListener('resize', handleResize)
131+
cancelAnimationFrame(resizeRaf)
132+
})
133+
</script>
134+
135+
<style scoped>
136+
.mindmap-fullscreen {
137+
position: fixed;
138+
top: 0;
139+
left: 0;
140+
right: 0;
141+
bottom: 0;
142+
width: 100vw;
143+
height: 100vh;
144+
background: #fff;
145+
z-index: 9999;
146+
}
147+
148+
#mindMapContainerFull * {
149+
margin: 0;
150+
padding: 0;
151+
}
152+
153+
.mind-map-container-full {
154+
width: 100%;
155+
height: 100%;
156+
background: #fff;
157+
}
158+
159+
.fullscreen-toolbar {
160+
position: fixed;
161+
top: 20px;
162+
right: 20px;
163+
z-index: 10000;
164+
display: flex;
165+
align-items: center;
166+
gap: 8px;
167+
background: rgba(255, 255, 255, 0.95);
168+
padding: 8px 12px;
169+
border-radius: 8px;
170+
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
171+
}
172+
173+
.toolbar-divider {
174+
width: 1px;
175+
height: 24px;
176+
background: #e5e7eb;
177+
margin: 0 4px;
178+
}
179+
</style>

0 commit comments

Comments
 (0)