统一连线样式并优化连接点位置

- 修改sub函数采用与main函数相同的二次贝塞尔曲线样式
- 统一所有连线的视觉风格,消除曲线和横线的混合
- 优化连接点位置,向内偏移31像素确保连线连接到节点边框内部
- 消除连线与节点之间的视觉间隔,达到完美贴合效果
This commit is contained in:
lixinran 2025-10-11 12:51:05 +08:00
parent d69def44ca
commit 4bb72ba6a4
13 changed files with 144 additions and 109 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -23,8 +23,8 @@
flex-direction: column; flex-direction: column;
} }
</style> </style>
<script type="module" crossorigin src="/assets/index-a814442b.js"></script> <script type="module" crossorigin src="/assets/index-a5a78393.js"></script>
<link rel="stylesheet" href="/assets/index-1f5435d2.css"> <link rel="stylesheet" href="/assets/index-2e253ee6.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@ -164,6 +164,7 @@
</div> </div>
</div> </div>
</div> </div>
</template> </template>
@ -172,7 +173,7 @@ import { ref, onMounted, onUnmounted, nextTick } from 'vue';
import MindElixir from '../lib/mind-elixir/dist/MindElixir.js'; import MindElixir from '../lib/mind-elixir/dist/MindElixir.js';
import '../lib/mind-elixir/dist/style.css'; import '../lib/mind-elixir/dist/style.css';
// //
const customTheme = { const customTheme = {
name: 'Light Purple', name: 'Light Purple',
type: 'light', type: 'light',
@ -209,6 +210,13 @@ const customTheme = {
'--panel-bgcolor': '#ffffff', '--panel-bgcolor': '#ffffff',
'--panel-border-color': '#eaeaea', '--panel-border-color': '#eaeaea',
'--map-padding': '50px', '--map-padding': '50px',
// -
'--enable-node-border': 'true',
'--node-border-padding': '8',
'--node-border-radius': '8',
'--node-border-color': '#660874',
'--node-border-fill': '#ffffff',
'--node-border-width': '1',
}, },
}; };
import { mindmapAPI } from '../api/mindmap.js'; import { mindmapAPI } from '../api/mindmap.js';
@ -250,6 +258,7 @@ const imagePreviewError = ref('');
// //
const showWelcomePage = () => { const showWelcomePage = () => {
showWelcome.value = true; showWelcome.value = true;
@ -351,6 +360,7 @@ const retryLoadImage = () => {
startImageLoadTimeout(); startImageLoadTimeout();
}; };
// //
let imageLoadTimeout = null; let imageLoadTimeout = null;
const startImageLoadTimeout = () => { const startImageLoadTimeout = () => {
@ -543,6 +553,7 @@ const loadMindmapData = async (data, keepPosition = false, shouldCenterRoot = tr
maxScale: 5, maxScale: 5,
minScale: 0.1, minScale: 0.1,
theme: customTheme, // theme: customTheme, //
enableNodeBorder: true, //
markdown: (text, nodeObj) => { markdown: (text, nodeObj) => {
// markdown // markdown
if (text.includes('|') || text.includes('**') || text.includes('`') || text.includes('#') || text.includes('![')) { if (text.includes('|') || text.includes('**') || text.includes('`') || text.includes('#') || text.includes('![')) {
@ -645,6 +656,7 @@ const loadMindmapData = async (data, keepPosition = false, shouldCenterRoot = tr
maxScale: 5, maxScale: 5,
minScale: 0.1, minScale: 0.1,
theme: customTheme, // theme: customTheme, //
enableNodeBorder: true, //
markdown: (text, nodeObj) => { markdown: (text, nodeObj) => {
// markdown // markdown
if (text.includes('|') || text.includes('**') || text.includes('`') || text.includes('#') || text.includes('![')) { if (text.includes('|') || text.includes('**') || text.includes('`') || text.includes('#') || text.includes('![')) {
@ -748,6 +760,7 @@ const loadMindmapData = async (data, keepPosition = false, shouldCenterRoot = tr
maxScale: 5, maxScale: 5,
minScale: 0.1, minScale: 0.1,
theme: customTheme, // theme: customTheme, //
enableNodeBorder: true, //
markdown: (text, nodeObj) => { markdown: (text, nodeObj) => {
// markdown // markdown
if (text.includes('|') || text.includes('**') || text.includes('`') || text.includes('#') || text.includes('![')) { if (text.includes('|') || text.includes('**') || text.includes('`') || text.includes('#') || text.includes('![')) {
@ -4867,5 +4880,6 @@ const updateMindMapRealtime = async (data, title) => {
.retry-button:hover { .retry-button:hover {
background: #7d0a8e; background: #7d0a8e;
} }
</style> </style>

View File

@ -77,11 +77,11 @@
margin: 10px; margin: 10px;
padding: 0; padding: 0;
& > me-tpc { & > me-tpc {
border-radius: var(--main-radius); border-radius: var(--main-radius) !important;
background-color: var(--main-bgcolor); background-color: var(--main-bgcolor) !important;
border: 2px solid var(--main-color); border: 2px solid var(--main-color) !important;
color: var(--main-color); color: var(--main-color) !important;
padding: 8px 25px; padding: 8px 25px !important;
} }
} }
} }
@ -118,9 +118,12 @@
z-index: 10; z-index: 10;
me-tpc { me-tpc {
position: relative; position: relative;
border-radius: 3px; border-radius: var(--main-radius, 20px); // 使用椭圆圆角
color: var(--color); color: var(--color);
padding: var(--topic-padding); padding: var(--topic-padding);
// 所有节点都使用紫色椭圆边框
background-color: var(--node-border-fill, #ffffff);
border: 2px solid var(--node-border-color, #660874);
// drag preview // drag preview
.insert-preview { .insert-preview {

View File

@ -42,6 +42,7 @@ function MindElixir(
handleWheel, handleWheel,
markdown, markdown,
imageProxy, imageProxy,
enableNodeBorder,
}: Options }: Options
): void { ): void {
let ele: HTMLElement | null = null let ele: HTMLElement | null = null
@ -93,6 +94,7 @@ function MindElixir(
this.handleWheel = handleWheel ?? true this.handleWheel = handleWheel ?? true
this.markdown = markdown || undefined // Custom markdown parser function this.markdown = markdown || undefined // Custom markdown parser function
this.imageProxy = imageProxy || undefined // Image proxy function this.imageProxy = imageProxy || undefined // Image proxy function
this.enableNodeBorder = enableNodeBorder ?? false // Node border feature
// this.parentMap = {} // deal with large amount of nodes // this.parentMap = {} // deal with large amount of nodes
this.currentNodes = [] // selected <tpc/> elements this.currentNodes = [] // selected <tpc/> elements
this.currentArrow = null // the selected link svg element this.currentArrow = null // the selected link svg element

View File

@ -41,6 +41,11 @@ export interface Topic extends HTMLElement {
image?: HTMLImageElement image?: HTMLImageElement
icons?: HTMLSpanElement icons?: HTMLSpanElement
tags?: HTMLDivElement tags?: HTMLDivElement
// 节点边框相关属性
borderRect?: SVGRectElement
borderObserver?: MutationObserver
borderResizeHandler?: () => void
} }
export interface Expander extends HTMLElement { export interface Expander extends HTMLElement {

View File

@ -56,6 +56,13 @@ export type Theme = {
'--panel-bgcolor': string '--panel-bgcolor': string
'--panel-border-color': string '--panel-border-color': string
'--map-padding': string '--map-padding': string
// 节点边框相关配置
'--enable-node-border'?: string
'--node-border-padding'?: string
'--node-border-radius'?: string
'--node-border-color'?: string
'--node-border-fill'?: string
'--node-border-width'?: string
} }
} }
@ -162,6 +169,11 @@ export interface Options {
* @default undefined * @default undefined
*/ */
imageProxy?: (url: string) => string imageProxy?: (url: string) => string
/**
* Enable node border feature
* @default false
*/
enableNodeBorder?: boolean
} }
export type Uid = string export type Uid = string

View File

@ -1,10 +1,11 @@
import { LEFT } from '../const' import { LEFT } from '../const'
import type { Topic, Wrapper, Parent, Children, Expander } from '../types/dom' import type { Topic, Wrapper, Parent, Children, Expander } from '../types/dom'
import type { MindElixirInstance, NodeObj } from '../types/index' import type { MindElixirInstance, NodeObj } from '../types/index'
import { encodeHTML } from '../utils/index' import { encodeHTML, getOffsetLT } from '../utils/index'
import { layoutChildren } from './layout' import { layoutChildren } from './layout'
// 移除imageProcessor引用使用MindElixir原生image属性 // 移除imageProcessor引用使用MindElixir原生image属性
// DOM manipulation // DOM manipulation
const $d = document const $d = document
export const findEle = function (this: MindElixirInstance, id: string, el?: HTMLElement) { export const findEle = function (this: MindElixirInstance, id: string, el?: HTMLElement) {
@ -175,9 +176,11 @@ export const createParent = function (this: MindElixirInstance, nodeObj: NodeObj
const tpc = this.createTopic(nodeObj) const tpc = this.createTopic(nodeObj)
shapeTpc.call(this, tpc, nodeObj) shapeTpc.call(this, tpc, nodeObj)
p.appendChild(tpc) p.appendChild(tpc)
return { p, tpc } return { p, tpc }
} }
export const createChildren = function (this: MindElixirInstance, wrappers: Wrapper[]) { export const createChildren = function (this: MindElixirInstance, wrappers: Wrapper[]) {
const children = $d.createElement('me-children') as Children const children = $d.createElement('me-children') as Children
children.append(...wrappers) children.append(...wrappers)

View File

@ -50,31 +50,27 @@ export function main({ pT, pL, pW, pH, cT, cL, cW, cH, direction, containerHeigh
} }
export function sub(this: MindElixirInstance, { pT, pL, pW, pH, cT, cL, cW, cH, direction, isFirst }: SubLineParams) { export function sub(this: MindElixirInstance, { pT, pL, pW, pH, cT, cL, cW, cH, direction, isFirst }: SubLineParams) {
const GAP = parseInt(this.container.style.getPropertyValue('--node-gap-x')) // cache? // 完全复制main函数的逻辑
// const GAP = 30 let x1 = pL + pW / 2
let y1 = 0 const y1 = pT + pH / 2
let end = 0 let x2
if (isFirst) {
y1 = pT + pH / 2
} else {
y1 = pT + pH
}
const y2 = cT + cH
let x1 = 0
let x2 = 0
let xMid = 0
const offset = (Math.abs(y1 - y2) / 300) * GAP
if (direction === DirectionClass.LHS) { if (direction === DirectionClass.LHS) {
xMid = pL x2 = cL + cW - 31 // 向内31像素确保连接到边框内部
x1 = xMid + GAP
x2 = xMid - GAP
end = cL + GAP
return `M ${x1} ${y1} C ${xMid} ${y1} ${xMid + offset} ${y2} ${x2} ${y2} H ${end}`
} else { } else {
xMid = pL + pW x2 = cL + 31 // 向内31像素确保连接到边框内部
x1 = xMid - GAP
x2 = xMid + GAP
end = cL + cW - GAP
return `M ${x1} ${y1} C ${xMid} ${y1} ${xMid - offset} ${y2} ${x2} ${y2} H ${end}`
} }
const y2 = cT + cH / 2
// 使用与main函数相同的偏移计算方式
const containerHeight = this.container.offsetHeight || 1000 // 提供一个默认值
const pct = Math.abs(y2 - y1) / containerHeight
const offset = (1 - pct) * 0.25 * (pW / 2)
if (direction === DirectionClass.LHS) {
x1 = x1 - pW / 10 - offset
} else {
x1 = x1 + pW / 10 + offset
}
return `M ${x1} ${y1} Q ${x1} ${y2} ${x2} ${y2}`
} }