Demo:外框样式设置改为侧边栏形式,并支持文字样式设置

This commit is contained in:
wanglin2
2025-03-30 15:15:46 +08:00
parent e5ee2f19d1
commit bb9dd123f1
6 changed files with 488 additions and 110 deletions

View File

@@ -4,6 +4,7 @@ import {
borderWidthList,
borderRadiusList,
lineWidthList,
lineHeightList,
store,
langList,
fontFamilyList as fontFamilyListZh,
@@ -166,6 +167,7 @@ export {
borderWidthList,
borderRadiusList,
lineWidthList,
lineHeightList,
store,
colorList,
langList,

View File

@@ -143,6 +143,9 @@ export const borderRadiusList = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// 线宽
export const lineWidthList = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// 行高
export const lineHeightList = [1, 1.2, 1.5, 2, 2.5, 3]
export const lineStyleMap = {
straight: `<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="60" height="26"><path d="M18,14L30,14L30,5L42,5" fill="none" stroke="#000" stroke-width="2"></path><path d="M18,14L30,14L30,23L42,23" fill="none" stroke="#000" stroke-width="2"></path></svg>`,
curve: `<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="60" height="26"><path d="M18,14L30,14A12,-9 0 0 1 42,5" fill="none" stroke="#000" stroke-width="2"></path><path d="M18,14L30,14A12,9 0 0 0 42,23" fill="none" stroke="#000" stroke-width="2"></path></svg>`,

View File

@@ -40,7 +40,8 @@ export default {
edge: 'Edge',
rainbowLines: 'Rainbow lines',
notUseRainbowLines: 'Not use rainbow lines',
outerFramePadding: 'Outer frame padding'
outerFramePadding: 'Outer frame padding',
associativeLineStyle: 'Associative line style'
},
setting: {
title: 'Setting',
@@ -74,8 +75,6 @@ export default {
watermarkTextOpacity: 'Text opacity',
watermarkTextFontSize: 'Font size',
belowNode: 'Display below nodes',
tagPositionRight: 'Text right',
tagPositionBottom: 'Text bottom',
alwaysShowExpandBtn: 'Always show expand btn',
enableAutoEnterTextEditWhenKeydown: 'Auto enter text edit when keydown',
confirm: 'Confirm',
@@ -420,11 +419,29 @@ export default {
animate: 'Animate'
},
nodeOuterFrame: {
outerFrameSetting: 'Setting',
outerFrameSetting: 'Outer frame setting',
deleteOuterFrame: 'Delete outer frame',
boxStyle: 'Box style',
boxColor: 'Box color',
fillColor: 'Fill color'
fillColor: 'Fill color',
nodeOuterFrameStyle: 'Outer frame style',
outerFrameText: 'Outer frame text',
deleteOuterFrameText: 'Delete outer frame text',
fontFamily: 'Font family',
color: 'Color',
fontSize: 'font size',
radius: 'Radius',
fontBold: 'Font bold',
italic: 'Italic',
lineHeight: 'Line height',
textFillRadius: 'Text fill radius',
textFill: 'Text fill color',
textAlign: 'Text align',
left: 'Left',
center: 'Center',
right: 'Right',
paddingX: 'Padding x',
paddingY: 'Padding y'
},
nodeTagStyle: {
placeholder: 'Please enter the tag content',

View File

@@ -39,7 +39,8 @@ export default {
edge: '边缘',
rainbowLines: '彩虹线条',
notUseRainbowLines: '不使用彩虹线条',
outerFramePadding: '外框内边距'
outerFramePadding: '外框内边距',
associativeLineStyle: '关联线样式'
},
setting: {
title: '设置',
@@ -72,8 +73,6 @@ export default {
watermarkTextOpacity: '文字透明度',
watermarkTextFontSize: '文字字号',
belowNode: '显示在节点下方',
tagPositionRight: '文本右侧',
tagPositionBottom: '文本下面',
alwaysShowExpandBtn: '是否一直显示展开收起按钮',
enableAutoEnterTextEditWhenKeydown: '键盘输入时自动进入文本编辑',
enableInheritAncestorLineStyle: '节点连线样式继承祖先节点的样式',
@@ -341,7 +340,8 @@ export default {
dragTip: '在此释放以导入该文件',
deleteNodeImgTip: '是否确认删除该节点图片?',
autoOpenNodeRichTextTip: '检测到导入了富文本内容,已自动开启富文本模式',
localStorageExceededTip: '你创建的思维导图体积已经超过浏览器允许存储的上限,请立即导出,否则数据将丢失!建议下载客户端进行使用,客户端无大小限制。'
localStorageExceededTip:
'你创建的思维导图体积已经超过浏览器允许存储的上限,请立即导出,否则数据将丢失!建议下载客户端进行使用,客户端无大小限制。'
},
mouseAction: {
tip1: '当前:左键拖动画布,右键框选节点',
@@ -407,11 +407,29 @@ export default {
animate: '开启动画'
},
nodeOuterFrame: {
outerFrameSetting: '外框设置',
nodeOuterFrameStyle: '外框样式',
outerFrameSetting: '外框',
deleteOuterFrame: '删除外框',
boxStyle: '边框样式',
boxColor: '边框颜色',
fillColor: '填充颜色'
fillColor: '填充颜色',
outerFrameText: '外框文字',
deleteOuterFrameText: '删除文字',
fontFamily: '字体',
color: '颜色',
fontSize: '字号',
radius: '圆角',
fontBold: '加粗',
italic: '斜体',
lineHeight: '行高',
textFillRadius: '背景圆角',
textFill: '背景填充',
textAlign: '显示位置',
left: '左',
center: '中',
right: '右',
paddingX: '水平内边距',
paddingY: '垂直内边距'
},
nodeTagStyle: {
placeholder: '请输入标签内容',

View File

@@ -41,8 +41,7 @@ export default {
rainbowLines: '彩虹線條',
notUseRainbowLines: '不使用彩虹線條',
outerFramePadding: '外框內距',
tagPositionRight: '文本右側',
tagPositionBottom: '文本下面'
associativeLineStyle: '關聯線樣式'
},
setting: {
title: '設置',
@@ -274,7 +273,8 @@ export default {
bottom: '下',
left: '左',
right: '右',
tag: '標簽'
tag: '標簽',
direction: '方向'
},
theme: {
title: '主題',
@@ -407,11 +407,29 @@ export default {
animate: '動畫'
},
nodeOuterFrame: {
outerFrameSetting: '外框設定',
outerFrameSetting: '外框',
deleteOuterFrame: '刪除外框',
boxStyle: '邊框樣式',
boxColor: '邊框顏色',
fillColor: '填充顏色'
fillColor: '填充顏色',
nodeOuterFrameStyle: '外框樣式',
outerFrameText: '外框文字',
deleteOuterFrameText: '刪除文字',
fontFamily: '字型',
color: '顏色',
fontSize: '字型大小',
radius: '圓角',
fontBold: '加粗',
italic: '斜體',
lineHeight: '行高',
textFillRadius: '背景圓角',
textFill: '背景填充',
textAlign: '顯示位置',
left: '左',
center: '中',
right: '右',
paddingX: '水平內邊距',
paddingY: '垂直內邊距'
},
nodeTagStyle: {
placeholder: '請輸入標籤內容',

View File

@@ -1,15 +1,6 @@
<template>
<div
class="nodeOuterFrameContainer"
ref="elRef"
:style="position"
v-show="show"
:class="{ isDark: isDark }"
>
<div class="btn" @click="showPanel = !showPanel">
<span class="iconfont iconjingzi"></span>
</div>
<div class="panel" v-if="showPanel">
<Sidebar ref="sidebar" :title="$t('nodeOuterFrame.nodeOuterFrameStyle')">
<div class="sidebarContent" :class="{ isDark: isDark }">
<div class="panelHeader">
<span class="name">{{ $t('nodeOuterFrame.outerFrameSetting') }}</span>
<span class="deleteBtn" @click="deleteOuterFrame">
@@ -105,6 +96,28 @@
></Color>
</el-popover>
</div>
<div class="rowItem">
<span class="name">{{ $t('nodeOuterFrame.radius') }}</span>
<el-select
size="mini"
style="width: 80px"
v-model="styleConfig.radius"
placeholder=""
@change="
value => {
updateOuterFrame('radius', value)
}
"
>
<el-option
v-for="item in borderRadiusList"
:key="item"
:label="item"
:value="item"
>
</el-option>
</el-select>
</div>
</div>
<div class="row">
<div class="rowItem">
@@ -127,18 +140,255 @@
</div>
</div>
</div>
<div class="panelHeader" style="margin-top: 12px;">
<span class="name">{{ $t('nodeOuterFrame.outerFrameText') }}</span>
<span class="deleteBtn" @click="deleteOuterFrameText">
{{ $t('nodeOuterFrame.deleteOuterFrameText') }}
<span class="iconfont iconshanchu"></span>
</span>
</div>
<div class="panelBody">
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('nodeOuterFrame.fontFamily') }}</span>
<el-select
size="mini"
v-model="styleConfig.fontFamily"
placeholder=""
@change="
value => {
updateOuterFrame('fontFamily', value)
}
"
>
<el-option
v-for="item in fontFamilyList"
:key="item.value"
:label="item.name"
:value="item.value"
:style="{ fontFamily: item.value }"
>
</el-option>
</el-select>
</div>
</div>
<div class="row">
<div class="btnGroup">
<el-tooltip
:content="$t('nodeOuterFrame.color')"
placement="bottom"
>
<div class="styleBtn" v-popover:popover3>
A
<span
class="colorShow"
:style="{ backgroundColor: styleConfig.color }"
></span>
</div>
</el-tooltip>
<el-tooltip
:content="$t('nodeOuterFrame.fontBold')"
placement="bottom"
>
<div
class="styleBtn"
:class="{
actived: styleConfig.fontWeight === 'bold'
}"
@click="toggleFontWeight"
>
B
</div>
</el-tooltip>
<el-tooltip
:content="$t('nodeOuterFrame.italic')"
placement="bottom"
>
<div
class="styleBtn i"
:class="{
actived: styleConfig.fontStyle === 'italic'
}"
@click="toggleFontStyle"
>
I
</div>
</el-tooltip>
</div>
<el-popover ref="popover3" placement="bottom" trigger="hover">
<Color
:color="styleConfig.color"
@change="
color => {
updateOuterFrame('color', color)
}
"
></Color>
</el-popover>
</div>
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('nodeOuterFrame.lineHeight') }}</span>
<el-select
size="mini"
style="width: 80px"
v-model="styleConfig.lineHeight"
placeholder=""
@change="
value => {
updateOuterFrame('lineHeight', value)
}
"
>
<el-option
v-for="item in lineHeightList"
:key="item"
:label="item"
:value="item"
>
</el-option>
</el-select>
</div>
<div class="rowItem">
<span class="name">{{ $t('nodeOuterFrame.fontSize') }}</span>
<el-select
size="mini"
style="width: 80px"
v-model="styleConfig.fontSize"
placeholder=""
@change="
color => {
updateOuterFrame('fontSize', color)
}
"
>
<el-option
v-for="item in fontSizeList"
:key="item"
:label="item"
:value="item"
:style="{ fontSize: item + 'px' }"
>
</el-option>
</el-select>
</div>
</div>
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('nodeOuterFrame.textFill') }}</span>
<span
class="block"
v-popover:popover4
:style="{ backgroundColor: styleConfig.textFill }"
></span>
<el-popover ref="popover4" placement="bottom" trigger="click">
<Color
:color="styleConfig.textFill"
@change="
color => {
updateOuterFrame('textFill', color)
}
"
></Color>
</el-popover>
</div>
<div class="rowItem">
<span class="name">{{ $t('nodeOuterFrame.textFillRadius') }}</span>
<el-select
size="mini"
style="width: 80px"
v-model="styleConfig.textFillRadius"
placeholder=""
@change="
value => {
updateOuterFrame('textFillRadius', value)
}
"
>
<el-option
v-for="item in borderRadiusList"
:key="item"
:label="item"
:value="item"
>
</el-option>
</el-select>
</div>
</div>
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('nodeOuterFrame.textAlign') }}</span>
<el-radio-group
v-model="styleConfig.textAlign"
size="mini"
@change="
value => {
updateOuterFrame('textAlign', value)
}
"
>
<el-radio-button label="left">{{
$t('nodeOuterFrame.left')
}}</el-radio-button>
<el-radio-button label="center">{{
$t('nodeOuterFrame.center')
}}</el-radio-button>
<el-radio-button label="right">{{
$t('nodeOuterFrame.right')
}}</el-radio-button>
</el-radio-group>
</div>
</div>
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('nodeOuterFrame.paddingX') }}</span>
<el-slider
style="width: 180px"
v-model="paddingStyle.paddingX"
@change="
value => {
updatePadding('x', value)
}
"
></el-slider>
</div>
</div>
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('nodeOuterFrame.paddingY') }}</span>
<el-slider
style="width: 180px"
v-model="paddingStyle.paddingY"
@change="
value => {
updatePadding('y', value)
}
"
></el-slider>
</div>
</div>
</div>
</div>
</div>
</Sidebar>
</template>
<script>
import Sidebar from './Sidebar.vue'
import Color from './Color.vue'
import { mapState } from 'vuex'
import { lineWidthList, borderDasharrayList } from '@/config'
import OuterFrame from 'simple-mind-map/src/plugins/OuterFrame';
import { mapState, mapMutations } from 'vuex'
import {
lineWidthList,
borderDasharrayList,
fontFamilyList,
fontSizeList,
borderRadiusList,
lineHeightList
} from '@/config'
import OuterFrame from 'simple-mind-map/src/plugins/OuterFrame'
export default {
components: {
Sidebar,
Color
},
props: {
@@ -149,24 +399,39 @@ export default {
data() {
return {
lineWidthList,
show: false,
showPanel: false,
position: {
left: 0,
top: 0
},
lineHeightList,
fontSizeList,
borderRadiusList,
styleConfig: {
...OuterFrame.defaultStyle
},
paddingStyle: {
paddingX: 0,
paddingY: 0
}
}
},
computed: {
...mapState({
activeSidebar: state => state.activeSidebar,
isDark: state => state.localConfig.isDark,
borderDasharrayList() {
return borderDasharrayList[this.$i18n.locale] || borderDasharrayList.zh
}
})
}),
fontFamilyList() {
return fontFamilyList[this.$i18n.locale] || fontFamilyList.zh
}
},
watch: {
activeSidebar(val) {
if (val === 'nodeOuterFrameStyle') {
this.$refs.sidebar.show = true
} else {
this.$refs.sidebar.show = false
}
}
},
created() {
this.mindMap.on('outer_frame_active', this.onOuterFrameActive)
@@ -175,6 +440,7 @@ export default {
this.mindMap.on('svg_mousedown', this.hide)
this.mindMap.on('expand_btn_click', this.hide)
this.mindMap.on('outer_frame_delete', this.hide)
this.mindMap.on('outer_frame_deactivate', this.hide)
},
beforeDestroy() {
this.mindMap.off('outer_frame_active', this.onOuterFrameActive)
@@ -183,11 +449,11 @@ export default {
this.mindMap.off('svg_mousedown', this.hide)
this.mindMap.off('expand_btn_click', this.hide)
this.mindMap.off('outer_frame_delete', this.hide)
},
mounted() {
document.body.appendChild(this.$refs.elRef)
this.mindMap.off('outer_frame_deactivate', this.hide)
},
methods: {
...mapMutations(['setActiveSidebar']),
onOuterFrameActive(el, parentNode, range) {
// 取范围内第一个节点的外框样式
const firstNode = parentNode.children[range[0]]
@@ -199,28 +465,52 @@ export default {
this.styleConfig[key] = OuterFrame.defaultStyle[key]
}
})
// 获取外框的位置大小信息
const { x, y, width } = el.rbox()
this.position.left = x + width + 'px'
this.position.top = y + 'px'
this.show = true
const [pl, pt] = this.styleConfig.textFillPadding
this.paddingStyle.paddingX = pl
this.paddingStyle.paddingY = pt
this.setActiveSidebar('nodeOuterFrameStyle')
},
updateOuterFrame(key, val) {
this.styleConfig[key] = val
this.mindMap.outerFrame.updateActiveOuterFrame({
[key]: val
})
this.hide()
})
},
// 切换加粗样式
toggleFontWeight() {
const newValue =
this.styleConfig.fontWeight === 'bold' ? 'normal' : 'bold'
this.updateOuterFrame('fontWeight', newValue)
},
// 切换字体样式
toggleFontStyle() {
const newValue =
this.styleConfig.fontStyle === 'italic' ? 'normal' : 'italic'
this.updateOuterFrame('fontStyle', newValue)
},
updatePadding(dir, value) {
const [pl, pt] = this.styleConfig.textFillPadding
if (dir === 'x') {
this.updateOuterFrame('textFillPadding', [value, pt, value, pt])
} else if (dir === 'y') {
this.updateOuterFrame('textFillPadding', [pl, value, pl, value])
}
},
deleteOuterFrame() {
this.mindMap.outerFrame.removeActiveOuterFrame()
},
deleteOuterFrameText() {
this.mindMap.outerFrame.removeActiveOuterFrameText()
},
hide() {
this.show = false
this.showPanel = false
this.setActiveSidebar(null)
}
}
}
@@ -234,9 +524,8 @@ export default {
}
</style>
<style lang="less" scoped>
.nodeOuterFrameContainer {
position: fixed;
transform: translate(-12px, -12px);
.sidebarContent {
padding: 20px;
&.isDark {
.panel {
@@ -274,73 +563,104 @@ export default {
border: 1px solid rgba(0, 0, 0, 0.06);
}
.panel {
position: absolute;
left: 0;
top: 24px;
background-color: #fff;
border-radius: 5px;
box-shadow: 0 2px 16px 0 rgba(0, 0, 0, 0.06);
border: 1px solid rgba(0, 0, 0, 0.06);
width: 250px;
padding: 12px;
.panelHeader {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
.panelHeader {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
.name {
font-size: 16px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: rgba(26, 26, 26, 0.9);
}
.deleteBtn {
display: flex;
align-items: center;
color: #909090;
font-size: 14px;
cursor: pointer;
user-select: none;
.iconfont {
margin-left: 2px;
font-size: 14px;
}
}
.name {
font-size: 16px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: rgba(26, 26, 26, 0.9);
}
.panelBody {
.row {
.deleteBtn {
display: flex;
align-items: center;
color: #909090;
font-size: 14px;
cursor: pointer;
user-select: none;
.iconfont {
margin-left: 2px;
font-size: 14px;
}
}
}
.panelBody {
.row {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
&:last-of-type {
margin-bottom: 0px;
}
.btnGroup {
width: 100%;
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
&:last-of-type {
margin-bottom: 0px;
.rowItem {
display: flex;
align-items: center;
.name {
font-size: 12px;
margin-right: 10px;
white-space: nowrap;
}
.rowItem {
display: flex;
align-items: center;
.block {
display: inline-block;
width: 20px;
height: 20px;
border: 1px solid #dcdfe6;
border-radius: 4px;
cursor: pointer;
}
}
.name {
font-size: 12px;
margin-right: 10px;
white-space: nowrap;
}
.styleBtn {
position: relative;
width: 50px;
height: 30px;
background: #fff;
border: 1px solid #eee;
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
cursor: pointer;
border-radius: 4px;
.block {
display: inline-block;
width: 20px;
height: 20px;
border: 1px solid #dcdfe6;
border-radius: 4px;
cursor: pointer;
}
&.actived {
background-color: #eee;
}
&.disabled {
background-color: #f5f7fa !important;
border-color: #e4e7ed !important;
color: #c0c4cc !important;
cursor: not-allowed !important;
}
&.i {
font-style: italic;
}
.colorShow {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 2px;
}
}
}