Demo:顶部工具栏支持根据窗口宽度自动收起到更多中

This commit is contained in:
wanglin2 2023-10-16 14:26:10 +08:00
parent 20157fcc8d
commit 83b916d3c9
5 changed files with 465 additions and 284 deletions

View File

@ -197,8 +197,8 @@ export default {
toolbar: {
undo: 'Undo',
redo: 'Redo',
insertSiblingNode: 'Insert sibling node',
insertChildNode: 'Insert child node',
insertSiblingNode: 'Sibling node',
insertChildNode: 'Child node',
deleteNode: 'Delete node',
image: 'Image',
icon: 'Icon',
@ -218,7 +218,8 @@ export default {
shortcutKey: 'Shortcut key',
associativeLine: 'Associative line',
painter: 'Painter',
formula: 'Formula'
formula: 'Formula',
more: 'More'
},
edit: {
newFeatureNoticeTitle: 'New feature reminder',

View File

@ -195,8 +195,8 @@ export default {
toolbar: {
undo: '回退',
redo: '前进',
insertSiblingNode: '插入同级节点',
insertChildNode: '插入子节点',
insertSiblingNode: '同级节点',
insertChildNode: '子节点',
deleteNode: '删除节点',
image: '图片',
icon: '图标',
@ -216,7 +216,8 @@ export default {
shortcutKey: '快捷键',
associativeLine: '关联线',
painter: '格式刷',
formula: '公式'
formula: '公式',
more: '更多'
},
edit: {
newFeatureNoticeTitle: '新特性提醒',

View File

@ -145,6 +145,7 @@ export default {
onLangChange(lang) {
i18n.locale = lang
storeLang(lang)
this.$bus.$emit('lang_change')
},
showSearch() {

View File

@ -1,149 +1,28 @@
<template>
<div class="toolbarContainer" :class="{ isDark: isDark }">
<div class="toolbar">
<div class="toolbar" ref="toolbarRef">
<!-- 节点操作 -->
<div class="toolbarBlock">
<div
class="toolbarBtn"
:class="{
disabled: readonly || backEnd
}"
@click="$bus.$emit('execCommand', 'BACK')"
<ToolbarNodeBtnList :list="horizontalList"></ToolbarNodeBtnList>
<!-- 更多 -->
<el-popover
v-model="popoverShow"
placement="bottom-end"
width="120"
trigger="hover"
v-if="showMoreBtn"
style="margin-left: 20px;"
>
<span class="icon iconfont iconhoutui-shi"></span>
<span class="text">{{ $t('toolbar.undo') }}</span>
</div>
<div
class="toolbarBtn"
:class="{
disabled: readonly || forwardEnd
}"
@click="$bus.$emit('execCommand', 'FORWARD')"
>
<span class="icon iconfont iconqianjin1"></span>
<span class="text">{{ $t('toolbar.redo') }}</span>
</div>
<div
class="toolbarBtn"
:class="{
disabled: activeNodes.length <= 0 || hasGeneralization,
active: isInPainter
}"
@click="$bus.$emit('startPainter')"
>
<span class="icon iconfont iconjiedian"></span>
<span class="text">{{ $t('toolbar.painter') }}</span>
</div>
<div
class="toolbarBtn"
:class="{
disabled: activeNodes.length <= 0 || hasRoot || hasGeneralization
}"
@click="$bus.$emit('execCommand', 'INSERT_NODE')"
>
<span class="icon iconfont iconjiedian"></span>
<span class="text">{{ $t('toolbar.insertSiblingNode') }}</span>
</div>
<div
class="toolbarBtn"
:class="{
disabled: activeNodes.length <= 0 || hasGeneralization
}"
@click="$bus.$emit('execCommand', 'INSERT_CHILD_NODE')"
>
<span class="icon iconfont icontianjiazijiedian"></span>
<span class="text">{{ $t('toolbar.insertChildNode') }}</span>
</div>
<div
class="toolbarBtn"
:class="{
disabled: activeNodes.length <= 0
}"
@click="$bus.$emit('execCommand', 'REMOVE_NODE')"
>
<span class="icon iconfont iconshanchu"></span>
<span class="text">{{ $t('toolbar.deleteNode') }}</span>
</div>
<div
class="toolbarBtn"
:class="{
disabled: activeNodes.length <= 0
}"
@click="$bus.$emit('showNodeImage')"
>
<span class="icon iconfont iconimage"></span>
<span class="text">{{ $t('toolbar.image') }}</span>
</div>
<div
class="toolbarBtn"
:class="{
disabled: activeNodes.length <= 0
}"
@click="showNodeIcon"
>
<span class="icon iconfont iconxiaolian"></span>
<span class="text">{{ $t('toolbar.icon') }}</span>
</div>
<div
class="toolbarBtn"
:class="{
disabled: activeNodes.length <= 0
}"
@click="$bus.$emit('showNodeLink')"
>
<span class="icon iconfont iconchaolianjie"></span>
<span class="text">{{ $t('toolbar.link') }}</span>
</div>
<div
class="toolbarBtn"
:class="{
disabled: activeNodes.length <= 0
}"
@click="$bus.$emit('showNodeNote')"
>
<span class="icon iconfont iconflow-Mark"></span>
<span class="text">{{ $t('toolbar.note') }}</span>
</div>
<div
class="toolbarBtn"
:class="{
disabled: activeNodes.length <= 0
}"
@click="$bus.$emit('showNodeTag')"
>
<span class="icon iconfont iconbiaoqian"></span>
<span class="text">{{ $t('toolbar.tag') }}</span>
</div>
<div
class="toolbarBtn"
:class="{
disabled: activeNodes.length <= 0 || hasRoot || hasGeneralization
}"
@click="$bus.$emit('execCommand', 'ADD_GENERALIZATION')"
>
<span class="icon iconfont icongaikuozonglan"></span>
<span class="text">{{ $t('toolbar.summary') }}</span>
</div>
<div
class="toolbarBtn"
:class="{
disabled: activeNodes.length <= 0 || hasGeneralization
}"
@click="$bus.$emit('createAssociativeLine')"
>
<span class="icon iconfont iconlianjiexian"></span>
<span class="text">{{ $t('toolbar.associativeLine') }}</span>
</div>
<div
class="toolbarBtn"
:class="{
disabled: activeNodes.length <= 0 || hasGeneralization
}"
@click="showFormula"
>
<span class="icon iconfont icongongshi"></span>
<span class="text">{{ $t('toolbar.formula') }}</span>
</div>
<ToolbarNodeBtnList
dir="v"
:list="verticalList"
@click.native="popoverShow = false"
></ToolbarNodeBtnList>
<div slot="reference" class="toolbarBtn">
<span class="icon iconfont icongongshi"></span>
<span class="text">{{ $t('toolbar.more') }}</span>
</div>
</el-popover>
</div>
<!-- 导出 -->
<div class="toolbarBlock">
@ -187,10 +66,12 @@ import NodeNote from './NodeNote'
import NodeTag from './NodeTag'
import Export from './Export'
import Import from './Import'
import { mapState, mapMutations } from 'vuex'
import { mapState } from 'vuex'
import { Notification } from 'element-ui'
import exampleData from 'simple-mind-map/example/exampleData'
import { getData } from '../../../api'
import ToolbarNodeBtnList from './ToolbarNodeBtnList.vue'
import { throttle } from 'simple-mind-map/src/utils/index'
/**
* @Author: 王林
@ -207,35 +88,35 @@ export default {
NodeNote,
NodeTag,
Export,
Import
Import,
ToolbarNodeBtnList
},
data() {
return {
activeNodes: [],
backEnd: false,
forwardEnd: true,
readonly: false,
isFullDataFile: false,
timer: null,
isInPainter: false
list: [
'back',
'forward',
'painter',
'siblingNode',
'childNode',
'deleteNode',
'image',
'icon',
'link',
'note',
'tag',
'summary',
'associativeLine',
'formula'
],
horizontalList: [],
verticalList: [],
showMoreBtn: true,
popoverShow: false
}
},
computed: {
...mapState(['isHandleLocalFile', 'isDark']),
hasRoot() {
return (
this.activeNodes.findIndex(node => {
return node.isRoot
}) !== -1
)
},
hasGeneralization() {
return (
this.activeNodes.findIndex(node => {
return node.isGeneralization
}) !== -1
)
}
...mapState(['isHandleLocalFile', 'isDark'])
},
watch: {
isHandleLocalFile(val) {
@ -245,68 +126,48 @@ export default {
}
},
created() {
this.$bus.$on('mode_change', this.onModeChange)
this.$bus.$on('node_active', this.onNodeActive)
this.$bus.$on('back_forward', this.onBackForward)
this.$bus.$on('write_local_file', this.onWriteLocalFile)
this.$bus.$on('painter_start', this.onPainterStart)
this.$bus.$on('painter_end', this.onPainterEnd)
},
mounted() {
this.computeToolbarShow()
this.computeToolbarShowThrottle = throttle(this.computeToolbarShow, 300)
window.addEventListener('resize', this.computeToolbarShowThrottle)
this.$bus.$on('lang_change', this.computeToolbarShowThrottle)
},
beforeDestroy() {
this.$bus.$off('mode_change', this.onModeChange)
this.$bus.$off('node_active', this.onNodeActive)
this.$bus.$off('back_forward', this.onBackForward)
this.$bus.$off('write_local_file', this.onWriteLocalFile)
this.$bus.$off('painter_start', this.onPainterStart)
this.$bus.$off('painter_end', this.onPainterEnd)
window.removeEventListener('resize', this.computeToolbarShowThrottle)
this.$bus.$off('lang_change', this.computeToolbarShowThrottle)
},
methods: {
...mapMutations(['setActiveSidebar']),
showNodeIcon() {
// this.$bus.$emit('showNodeIcon')
this.$bus.$emit('close_node_icon_toolbar')
this.setActiveSidebar('nodeIconSidebar')
//
computeToolbarShow() {
const windowWidth = window.innerWidth - 40
const all = [...this.list]
let index = 1
const loopCheck = () => {
if (index > all.length) return done()
this.horizontalList = all.slice(0, index)
this.$nextTick(() => {
const width = this.$refs.toolbarRef.getBoundingClientRect().width
if (width < windowWidth) {
index++
loopCheck()
} else if (index > 0 && width > windowWidth) {
index--
this.horizontalList = all.slice(0, index)
done()
}
})
}
const done = () => {
this.verticalList = all.slice(index)
this.showMoreBtn = this.verticalList.length > 0
}
loopCheck()
},
//
showFormula() {
this.setActiveSidebar('formulaSidebar')
},
/**
* @Author: 王林25
* @Date: 2022-11-14 19:17:40
* @Desc: 监听模式切换
*/
onModeChange(mode) {
this.readonly = mode === 'readonly'
},
/**
* @Author: 王林25
* @Date: 2022-11-14 19:18:06
* @Desc: 监听节点激活
*/
onNodeActive(...args) {
this.activeNodes = [...args[1]]
},
/**
* @Author: 王林25
* @Date: 2022-11-14 19:18:31
* @Desc: 监听前进后退
*/
onBackForward(index, len) {
this.backEnd = index <= 0
this.forwardEnd = index >= len - 1
},
/**
* @Author: 王林25
* @Date: 2022-11-14 19:19:14
* @Desc: 监听本地文件读写
*/
//
onWriteLocalFile(content) {
clearTimeout(this.timer)
this.timer = setTimeout(() => {
@ -314,11 +175,7 @@ export default {
}, 1000)
},
/**
* @Author: 王林
* @Date: 2022-09-24 15:40:09
* @Desc: 打开本地文件
*/
//
async openLocalFile() {
try {
let [_fileHandle] = await window.showOpenFilePicker({
@ -353,11 +210,7 @@ export default {
}
},
/**
* @Author: 王林
* @Date: 2022-09-24 15:40:18
* @Desc: 读取本地文件
*/
//
async readFile() {
let file = await fileHandle.getFile()
let fileReader = new FileReader()
@ -375,11 +228,7 @@ export default {
fileReader.readAsText(file)
},
/**
* @Author: 王林
* @Date: 2022-09-24 15:40:26
* @Desc: 渲染读取的数据
*/
//
setData(str) {
try {
let data = JSON.parse(str)
@ -402,11 +251,7 @@ export default {
}
},
/**
* @Author: 王林
* @Date: 2022-09-24 15:40:42
* @Desc: 写入本地文件
*/
//
async writeLocalFile(content) {
if (!fileHandle || !this.isHandleLocalFile) {
return
@ -420,30 +265,18 @@ export default {
await writable.close()
},
/**
* @Author: 王林
* @Date: 2022-09-24 15:40:48
* @Desc: 创建本地文件
*/
//
async createNewLocalFile() {
await this.createLocalFile(exampleData)
},
/**
* @Author: 王林
* @Date: 2022-09-24 15:49:17
* @Desc: 另存为
*/
//
async saveLocalFile() {
let data = getData()
await this.createLocalFile(data)
},
/**
* @Author: 王林
* @Date: 2022-09-24 15:50:22
* @Desc: 创建本地文件
*/
//
async createLocalFile(content) {
try {
let _fileHandle = await window.showSaveFilePicker({
@ -479,14 +312,6 @@ export default {
'你的浏览器可能不支持建议使用最新版本的Chrome浏览器'
)
}
},
onPainterStart() {
this.isInPainter = true
},
onPainterEnd() {
this.isInPainter = false
}
}
}
@ -496,7 +321,7 @@ export default {
.toolbarContainer {
&.isDark {
.toolbar {
color: hsla(0,0%,100%,.9);
color: hsla(0, 0%, 100%, 0.9);
.toolbarBlock {
background-color: #262a2e;
}
@ -510,7 +335,7 @@ export default {
&:hover {
&:not(.disabled) {
.icon {
background: hsla(0,0%,100%,.05);
background: hsla(0, 0%, 100%, 0.05);
}
}
}
@ -527,14 +352,12 @@ export default {
transform: translateX(-50%);
top: 20px;
width: max-content;
max-width: 100%;
display: flex;
font-size: 12px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: rgba(26, 26, 26, 0.8);
z-index: 2;
overflow-x: auto;
.toolbarBlock {
display: flex;
@ -600,16 +423,4 @@ export default {
}
}
}
@media screen and (max-width: 1040px) {
.toolbarContainer {
.toolbar {
left: 20px;
right: 20px;
transform: translateX(0);
width: auto;
max-width: none;
}
}
}
</style>

View File

@ -0,0 +1,367 @@
<template>
<div class="toolbarNodeBtnList" :class="[dir, { isDark: isDark }]">
<template v-for="item in list">
<div
v-if="item === 'back'"
class="toolbarBtn"
:class="{
disabled: readonly || backEnd
}"
@click="$bus.$emit('execCommand', 'BACK')"
>
<span class="icon iconfont iconhoutui-shi"></span>
<span class="text">{{ $t('toolbar.undo') }}</span>
</div>
<div
v-if="item === 'forward'"
class="toolbarBtn"
:class="{
disabled: readonly || forwardEnd
}"
@click="$bus.$emit('execCommand', 'FORWARD')"
>
<span class="icon iconfont iconqianjin1"></span>
<span class="text">{{ $t('toolbar.redo') }}</span>
</div>
<div
v-if="item === 'painter'"
class="toolbarBtn"
:class="{
disabled: activeNodes.length <= 0 || hasGeneralization,
active: isInPainter
}"
@click="$bus.$emit('startPainter')"
>
<span class="icon iconfont iconjiedian"></span>
<span class="text">{{ $t('toolbar.painter') }}</span>
</div>
<div
v-if="item === 'siblingNode'"
class="toolbarBtn"
:class="{
disabled: activeNodes.length <= 0 || hasRoot || hasGeneralization
}"
@click="$bus.$emit('execCommand', 'INSERT_NODE')"
>
<span class="icon iconfont iconjiedian"></span>
<span class="text">{{ $t('toolbar.insertSiblingNode') }}</span>
</div>
<div
v-if="item === 'childNode'"
class="toolbarBtn"
:class="{
disabled: activeNodes.length <= 0 || hasGeneralization
}"
@click="$bus.$emit('execCommand', 'INSERT_CHILD_NODE')"
>
<span class="icon iconfont icontianjiazijiedian"></span>
<span class="text">{{ $t('toolbar.insertChildNode') }}</span>
</div>
<div
v-if="item === 'deleteNode'"
class="toolbarBtn"
:class="{
disabled: activeNodes.length <= 0
}"
@click="$bus.$emit('execCommand', 'REMOVE_NODE')"
>
<span class="icon iconfont iconshanchu"></span>
<span class="text">{{ $t('toolbar.deleteNode') }}</span>
</div>
<div
v-if="item === 'image'"
class="toolbarBtn"
:class="{
disabled: activeNodes.length <= 0
}"
@click="$bus.$emit('showNodeImage')"
>
<span class="icon iconfont iconimage"></span>
<span class="text">{{ $t('toolbar.image') }}</span>
</div>
<div
v-if="item === 'icon'"
class="toolbarBtn"
:class="{
disabled: activeNodes.length <= 0
}"
@click="showNodeIcon"
>
<span class="icon iconfont iconxiaolian"></span>
<span class="text">{{ $t('toolbar.icon') }}</span>
</div>
<div
v-if="item === 'link'"
class="toolbarBtn"
:class="{
disabled: activeNodes.length <= 0
}"
@click="$bus.$emit('showNodeLink')"
>
<span class="icon iconfont iconchaolianjie"></span>
<span class="text">{{ $t('toolbar.link') }}</span>
</div>
<div
v-if="item === 'note'"
class="toolbarBtn"
:class="{
disabled: activeNodes.length <= 0
}"
@click="$bus.$emit('showNodeNote')"
>
<span class="icon iconfont iconflow-Mark"></span>
<span class="text">{{ $t('toolbar.note') }}</span>
</div>
<div
v-if="item === 'tag'"
class="toolbarBtn"
:class="{
disabled: activeNodes.length <= 0
}"
@click="$bus.$emit('showNodeTag')"
>
<span class="icon iconfont iconbiaoqian"></span>
<span class="text">{{ $t('toolbar.tag') }}</span>
</div>
<div
v-if="item === 'summary'"
class="toolbarBtn"
:class="{
disabled: activeNodes.length <= 0 || hasRoot || hasGeneralization
}"
@click="$bus.$emit('execCommand', 'ADD_GENERALIZATION')"
>
<span class="icon iconfont icongaikuozonglan"></span>
<span class="text">{{ $t('toolbar.summary') }}</span>
</div>
<div
v-if="item === 'associativeLine'"
class="toolbarBtn"
:class="{
disabled: activeNodes.length <= 0 || hasGeneralization
}"
@click="$bus.$emit('createAssociativeLine')"
>
<span class="icon iconfont iconlianjiexian"></span>
<span class="text">{{ $t('toolbar.associativeLine') }}</span>
</div>
<div
v-if="item === 'formula'"
class="toolbarBtn"
:class="{
disabled: activeNodes.length <= 0 || hasGeneralization
}"
@click="showFormula"
>
<span class="icon iconfont icongongshi"></span>
<span class="text">{{ $t('toolbar.formula') }}</span>
</div>
</template>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
export default {
props: {
dir: {
type: String,
default: 'h' // hv
},
list: {
type: Array,
default() {
return []
}
}
},
data() {
return {
activeNodes: [],
backEnd: false,
forwardEnd: true,
readonly: false,
isFullDataFile: false,
timer: null,
isInPainter: false
}
},
computed: {
...mapState(['isDark']),
hasRoot() {
return (
this.activeNodes.findIndex(node => {
return node.isRoot
}) !== -1
)
},
hasGeneralization() {
return (
this.activeNodes.findIndex(node => {
return node.isGeneralization
}) !== -1
)
}
},
created() {
this.$bus.$on('mode_change', this.onModeChange)
this.$bus.$on('node_active', this.onNodeActive)
this.$bus.$on('back_forward', this.onBackForward)
this.$bus.$on('painter_start', this.onPainterStart)
this.$bus.$on('painter_end', this.onPainterEnd)
},
beforeDestroy() {
this.$bus.$off('mode_change', this.onModeChange)
this.$bus.$off('node_active', this.onNodeActive)
this.$bus.$off('back_forward', this.onBackForward)
this.$bus.$off('painter_start', this.onPainterStart)
this.$bus.$off('painter_end', this.onPainterEnd)
},
methods: {
...mapMutations(['setActiveSidebar']),
//
onModeChange(mode) {
this.readonly = mode === 'readonly'
},
//
onNodeActive(...args) {
this.activeNodes = [...args[1]]
},
// 退
onBackForward(index, len) {
this.backEnd = index <= 0
this.forwardEnd = index >= len - 1
},
//
onPainterStart() {
this.isInPainter = true
},
//
onPainterEnd() {
this.isInPainter = false
},
//
showNodeIcon() {
this.$bus.$emit('close_node_icon_toolbar')
this.setActiveSidebar('nodeIconSidebar')
},
//
showFormula() {
this.setActiveSidebar('formulaSidebar')
}
}
}
</script>
<style lang="less" scoped>
.toolbarNodeBtnList {
display: flex;
&.isDark {
.toolbarBtn {
color: hsla(0, 0%, 100%, 0.9);
.icon {
background: transparent;
border-color: transparent;
}
&:hover {
&:not(.disabled) {
.icon {
background: hsla(0, 0%, 100%, 0.05);
}
}
}
&.disabled {
color: #54595f;
}
}
}
.toolbarBtn {
display: flex;
justify-content: center;
flex-direction: column;
cursor: pointer;
margin-right: 20px;
&:last-of-type {
margin-right: 0;
}
&:hover {
&:not(.disabled) {
.icon {
background: #f5f5f5;
}
}
}
&.active {
.icon {
background: #f5f5f5;
}
}
&.disabled {
color: #bcbcbc;
cursor: not-allowed;
pointer-events: none;
}
.icon {
display: flex;
height: 26px;
background: #fff;
border-radius: 4px;
border: 1px solid #e9e9e9;
justify-content: center;
flex-direction: column;
text-align: center;
padding: 0 5px;
}
.text {
margin-top: 3px;
}
}
&.v {
display: block;
width: 120px;
flex-wrap: wrap;
.toolbarBtn {
flex-direction: row;
justify-content: flex-start;
margin-bottom: 10px;
width: 100%;
margin-right: 0;
&:last-of-type {
margin-bottom: 0;
}
.icon {
margin-right: 10px;
}
.text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
</style>