Demo:新增源码编辑模式

This commit is contained in:
街角小林
2024-03-27 19:13:59 +08:00
parent bff683cb5c
commit 102cbeb821
14 changed files with 299 additions and 10 deletions

11
web/package-lock.json generated
View File

@@ -9,6 +9,7 @@
"version": "0.1.0",
"dependencies": {
"@toast-ui/editor": "^3.1.5",
"codemirror": "^5.65.16",
"core-js": "^3.6.5",
"element-ui": "^2.15.1",
"highlight.js": "^10.7.3",
@@ -5009,6 +5010,11 @@
"node": ">= 4.0"
}
},
"node_modules/codemirror": {
"version": "5.65.16",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.16.tgz",
"integrity": "sha512-br21LjYmSlVL0vFCPWPfhzUCT34FM/pAdK7rRIZwa0rrtrIdotvP4Oh4GUHsu2E3IrQMCfRkL/fN3ytMNxVQvg=="
},
"node_modules/codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
@@ -21021,6 +21027,11 @@
"q": "^1.1.2"
}
},
"codemirror": {
"version": "5.65.16",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.16.tgz",
"integrity": "sha512-br21LjYmSlVL0vFCPWPfhzUCT34FM/pAdK7rRIZwa0rrtrIdotvP4Oh4GUHsu2E3IrQMCfRkL/fN3ytMNxVQvg=="
},
"codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",

View File

@@ -14,6 +14,7 @@
},
"dependencies": {
"@toast-ui/editor": "^3.1.5",
"codemirror": "^5.65.16",
"core-js": "^3.6.5",
"element-ui": "^2.15.1",
"highlight.js": "^10.7.3",

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 2479351 */
src: url('iconfont.woff2?t=1711432586441') format('woff2'),
url('iconfont.woff?t=1711432586441') format('woff'),
url('iconfont.ttf?t=1711432586441') format('truetype');
src: url('iconfont.woff2?t=1711536835850') format('woff2'),
url('iconfont.woff?t=1711536835850') format('woff'),
url('iconfont.ttf?t=1711536835850') format('truetype');
}
.iconfont {
@@ -13,6 +13,14 @@
-moz-osx-font-smoothing: grayscale;
}
.icongeshihua:before {
content: "\e7a3";
}
.iconyuanma:before {
content: "\e658";
}
.icongundongtiao:before {
content: "\e670";
}

View File

@@ -152,7 +152,8 @@ export default {
closeMiniMap: 'Close mini map',
readonly: 'Change to eadonly',
edit: 'Change to edit',
backToRoot: 'Back to root node'
backToRoot: 'Back to root node',
changeSourceCodeEdit: 'Switch to source code editing mode'
},
nodeHyperlink: {
title: 'Link',
@@ -317,5 +318,15 @@ export default {
},
other: {
loading: 'Loading, please wait...'
},
sourceCodeEdit: {
sourceCodeTip: 'It is not recommended to modify the style in rich text mode because it requires synchronous modification of data and HTML structure.',
format: 'Format',
copy: 'Copy',
confirm: 'Complete',
close: 'Close',
formatErrorTip: 'The JSON format is incorrect. Please check and try again',
copyTip: 'Copied to clipboard',
formatTip: 'Format complete'
}
}

View File

@@ -150,7 +150,8 @@ export default {
closeMiniMap: '关闭小地图',
readonly: '切换为只读模式',
edit: '切换为编辑模式',
backToRoot: '回到根节点'
backToRoot: '回到根节点',
changeSourceCodeEdit: '切换为源码编辑模式'
},
nodeHyperlink: {
title: '超链接',
@@ -311,5 +312,15 @@ export default {
},
other: {
loading: '正在加载,请稍后...'
},
sourceCodeEdit: {
sourceCodeTip: '富文本模式下不建议修改样式因为需要同步修改数据及html结构。',
format: '格式化',
copy: '复制',
confirm: '完成',
close: '关闭',
formatErrorTip: 'JSON格式有误请检查后再试',
copyTip: '已复制到剪贴板',
formatTip: '格式化完成'
}
}

View File

@@ -24,6 +24,7 @@
<OutlineEdit v-if="mindMap" :mindMap="mindMap"></OutlineEdit>
<Scrollbar v-if="isShowScrollbar && mindMap" :mindMap="mindMap"></Scrollbar>
<FormulaSidebar v-if="mindMap" :mindMap="mindMap"></FormulaSidebar>
<SourceCodeEdit v-if="mindMap" :mindMap="mindMap"></SourceCodeEdit>
</div>
</template>
@@ -83,6 +84,7 @@ import handleClipboardText from '@/utils/handleClipboardText'
import Scrollbar from './Scrollbar.vue'
import exampleData from 'simple-mind-map/example/exampleData'
import FormulaSidebar from './FormulaSidebar.vue'
import SourceCodeEdit from './SourceCodeEdit.vue'
// 注册插件
MindMap.usePlugin(MiniMap)
@@ -134,7 +136,8 @@ export default {
NodeIconToolbar,
OutlineEdit,
Scrollbar,
FormulaSidebar
FormulaSidebar,
SourceCodeEdit
},
data() {
return {

View File

@@ -80,6 +80,15 @@
@click="toggleDark"
></div>
</div>
<div class="item">
<el-tooltip
effect="dark"
:content="$t('navigatorToolbar.changeSourceCodeEdit')"
placement="top"
>
<div class="btn iconfont iconyuanma" @click="openSourceCodeEdit"></div>
</el-tooltip>
</div>
<div class="item">
<el-dropdown @command="handleCommand">
<div class="btn iconfont iconbangzhu"></div>
@@ -141,7 +150,7 @@ export default {
this.lang = getLang()
},
methods: {
...mapMutations(['setLocalConfig', 'setIsReadonly']),
...mapMutations(['setLocalConfig', 'setIsReadonly', 'setIsOutlineEdit']),
readonlyChange() {
this.setIsReadonly(!this.isReadonly)
@@ -198,6 +207,10 @@ export default {
backToRoot() {
this.mindMap.renderer.setRootNodeCenter()
},
openSourceCodeEdit() {
this.setIsOutlineEdit(true)
}
}
}

View File

@@ -35,7 +35,6 @@ export default {
computed: {
...mapState({
isDark: state => state.localConfig.isDark,
isOutlineEdit: state => state.isOutlineEdit,
activeSidebar: state => state.activeSidebar
})
},

View File

@@ -0,0 +1,216 @@
<template>
<div
class="sourceCodeEditContainer"
:class="{ isDark: isDark }"
ref="sourceCodeEditContainer"
v-if="isSourceCodeEdit"
>
<div class="closeBtn">
<el-tooltip
effect="dark"
:content="$t('sourceCodeEdit.copy')"
placement="top"
>
<span
class="icon iconfont iconfuzhi"
style="font-size: 26px"
@click="copy"
></span>
</el-tooltip>
<el-tooltip
effect="dark"
:content="$t('sourceCodeEdit.format')"
placement="top"
>
<span
class="icon iconfont icongeshihua"
style="font-size: 24px"
@click="format"
></span>
</el-tooltip>
<el-tooltip
effect="dark"
:content="$t('sourceCodeEdit.sourceCodeTip')"
placement="top"
>
<span class="icon el-icon-info"></span>
</el-tooltip>
<el-tooltip
effect="dark"
:content="$t('sourceCodeEdit.confirm')"
placement="top"
>
<span class="icon el-icon-circle-check" @click="onConfirm"></span>
</el-tooltip>
<el-tooltip
effect="dark"
:content="$t('sourceCodeEdit.close')"
placement="top"
>
<span class="icon iconfont iconguanbi" @click="onClose"></span>
</el-tooltip>
</div>
<div class="sourceCodeEditBox">
<div class="outlineEdit" ref="outlineEditRef" @keydown.stop></div>
</div>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
import { storeData } from '@/api'
import CodeMirror from 'codemirror'
import 'codemirror/mode/javascript/javascript'
import 'codemirror/lib/codemirror.css'
import { copy } from '@/utils/index'
let editor = null
// 源码编辑
export default {
name: 'SourceCodeEdit',
props: {
mindMap: {
type: Object
}
},
data() {
return {}
},
computed: {
...mapState({
isReadonly: state => state.isReadonly,
isDark: state => state.localConfig.isDark,
isSourceCodeEdit: state => state.isSourceCodeEdit
})
},
watch: {
isSourceCodeEdit(val) {
if (val) {
this.$nextTick(() => {
document.body.appendChild(this.$refs.sourceCodeEditContainer)
this.initEditor()
this.initData()
})
}
}
},
methods: {
...mapMutations(['setIsOutlineEdit']),
// 初始化编辑器
initEditor() {
editor = CodeMirror(this.$refs.outlineEditRef, {
mode: { name: 'javascript', json: true },
lineWrapping: true,
lineNumbers: true
})
},
// 初始化数据
initData() {
editor.setValue(JSON.stringify(this.mindMap.getData(), null, 2))
},
// 完成
onConfirm() {
try {
const content = editor.getValue()
const data = JSON.parse(content)
this.setIsOutlineEdit(false)
this.$bus.$emit('setData', data)
} catch (error) {
console.log(error)
this.$message.error(this.$t('sourceCodeEdit.formatErrorTip'))
}
},
// 关闭
onClose() {
this.setIsOutlineEdit(false)
},
// 复制
copy() {
const content = editor.getValue()
copy(content)
this.$message.success(this.$t('sourceCodeEdit.copyTip'))
},
// 格式化
format() {
try {
const content = editor.getValue()
const data = JSON.parse(content)
editor.setValue(JSON.stringify(data, null, 2))
this.$message.success(this.$t('sourceCodeEdit.formatTip'))
} catch (error) {
console.log(error)
this.$message.error(this.$t('sourceCodeEdit.formatErrorTip'))
}
}
}
}
</script>
<style lang="less" scoped>
.sourceCodeEditContainer {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 1999;
background-color: #f5f5f5;
overflow: hidden;
&.isDark {
background-color: #262a2e;
.closeBtn {
.icon {
color: #fff;
}
}
}
.closeBtn {
position: absolute;
right: 40px;
top: 20px;
cursor: pointer;
display: flex;
align-items: center;
.icon {
font-size: 28px;
margin-left: 10px;
}
}
.sourceCodeEditBox {
width: 100%;
height: 100%;
overflow: hidden;
padding: 50px 0;
.outlineEdit {
width: 1000px;
height: 100%;
margin: 0 auto;
font-size: 17px;
background-color: #fff;
font-family: Menlo, Monaco, Consolas, Andale Mono, Ubuntu Mono,
Courier New, monospace;
padding: 12px;
border-radius: 5px;
/deep/ .CodeMirror {
height: 100%;
font-family: Menlo, Monaco, Consolas, Andale Mono, Ubuntu Mono,
Courier New, monospace;
}
}
}
}
</style>

View File

@@ -25,7 +25,8 @@ const store = new Vuex.Store({
},
activeSidebar: '', // 当前显示的侧边栏
isOutlineEdit: false, // 是否是大纲编辑模式
isReadonly: false // 是否只读
isReadonly: false, // 是否只读
isSourceCodeEdit: false// 是否是源码编辑模式
},
mutations: {
// 设置思维导图数据
@@ -60,7 +61,12 @@ const store = new Vuex.Store({
// 设置是否只读
setIsReadonly(state, data) {
state.isReadonly = data
}
},
// 设置源码编辑模式
setIsOutlineEdit(state, data) {
state.isSourceCodeEdit = data
},
},
actions: {
// 设置初始思维导图数据

View File

@@ -47,3 +47,13 @@ export const fileToBuffer = file => {
reader.readAsArrayBuffer(file)
})
}
// 复制文本到剪贴板
export const copy = (text) => {
const input = document.createElement('input')
input.setAttribute('value', text)
document.body.appendChild(input)
input.select()
document.execCommand('copy')
document.body.removeChild(input)
}