✨ 优化表格编辑器UI和用户体验
改进内容: - 使用fixed定位的模态框,居中显示 - 添加半透明遮罩层,突出编辑器 - 增大编辑器尺寸(600x400px),更舒适的编辑空间 - 设置明显的光标颜色(caret-color: #007bff) - 添加编辑器标题和快捷键提示 - 优化按钮样式,添加hover效果 - 设置textarea背景色(#fafafa),增强可见性 - 禁用拼写检查和自动完成 - 延迟聚焦确保光标可见 - 点击遮罩层可关闭编辑器
This commit is contained in:
parent
1c838a9aeb
commit
1f67b9ae58
Binary file not shown.
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
|
|
@ -23,8 +23,8 @@
|
|||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/index-fc166542.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index-bafd0b52.css">
|
||||
<script type="module" crossorigin src="/assets/index-78cb04db.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index-ae3a3afb.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
|
|||
|
|
@ -875,8 +875,8 @@ const formatMarkdownToText = (markdown) => {
|
|||
.replace(/`(.*?)`/g, '「$1」')
|
||||
// 处理链接
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, '🔗 $1')
|
||||
// 图片通过MindElixir原生image属性处理,保留图片占位符用于后续处理
|
||||
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '[图片: $1]')
|
||||
// 图片现在通过markdown编辑器渲染,保留图片markdown语法
|
||||
// .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '[图片: $1]') // 注释掉,保留图片markdown语法
|
||||
// 处理换行 - 优化换行处理,避免错位
|
||||
.replace(/\n\n/g, '\n')
|
||||
.replace(/\n/g, '\n ')
|
||||
|
|
@ -953,9 +953,10 @@ const extractImageFromContent = (content) => {
|
|||
return images;
|
||||
};
|
||||
|
||||
// 从内容中移除图片Markdown语法
|
||||
// 从内容中移除图片Markdown语法(现在保留,因为图片节点使用markdown渲染)
|
||||
const removeImageMarkdown = (content) => {
|
||||
return content.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '').trim();
|
||||
// 不再移除图片markdown语法,保留用于markdown渲染
|
||||
return content.trim();
|
||||
};
|
||||
|
||||
// Markdown转JSON的核心逻辑 - 智能层次化版本
|
||||
|
|
@ -1003,17 +1004,24 @@ const markdownToJSON = (markdown) => {
|
|||
data: {},
|
||||
};
|
||||
|
||||
// 如果标题中有图片,使用第一个图片作为节点图片
|
||||
// 如果标题中有图片,使用markdown渲染
|
||||
if (titleImages.length > 0) {
|
||||
const firstImage = titleImages[0];
|
||||
if (firstImage.url && firstImage.url.trim() !== '') {
|
||||
node.image = {
|
||||
url: firstImage.url,
|
||||
width: 200,
|
||||
height: 150,
|
||||
fit: 'contain'
|
||||
};
|
||||
console.log(`✅ 成功为标题节点设置图片: ${firstImage.url}`);
|
||||
// 转换图片URL为代理URL
|
||||
let imageUrl = firstImage.url;
|
||||
if (imageUrl.includes('cdn-mineru.openxlab.org.cn')) {
|
||||
const urlPath = imageUrl.replace('https://cdn-mineru.openxlab.org.cn', '');
|
||||
imageUrl = `/proxy-image${urlPath}`;
|
||||
}
|
||||
|
||||
// 使用markdown渲染标题中的图片
|
||||
const titleMarkdown = ``;
|
||||
const htmlContent = renderMarkdownToHTML(titleMarkdown);
|
||||
|
||||
node.topic = ''; // 清空topic
|
||||
node.dangerouslySetInnerHTML = htmlContent; // 使用dangerouslySetInnerHTML
|
||||
console.log(`✅ 成功为标题节点设置图片: ${imageUrl}`);
|
||||
} else {
|
||||
console.error(`❌ 标题图片URL无效:`, firstImage);
|
||||
}
|
||||
|
|
@ -1094,28 +1102,28 @@ const processContentIntelligently = (content, parentNode, nodeCounter) => {
|
|||
console.log(`🔄 转换图片URL: ${image.url} -> ${imageUrl}`);
|
||||
}
|
||||
|
||||
// 使用markdown渲染图片节点,统一技术栈
|
||||
const imageMarkdown = ``;
|
||||
const htmlContent = renderMarkdownToHTML(imageMarkdown);
|
||||
console.log('🖼️ 图片HTML内容:', htmlContent);
|
||||
|
||||
const imageNode = {
|
||||
id: `node_${nodeCounter++}`,
|
||||
topic: image.alt || `图片 ${index + 1}`,
|
||||
topic: '', // 清空topic,使用dangerouslySetInnerHTML
|
||||
dangerouslySetInnerHTML: htmlContent, // 使用dangerouslySetInnerHTML来正确处理HTML内容
|
||||
children: [],
|
||||
level: (parentNode.level || 0) + 1,
|
||||
image: {
|
||||
url: imageUrl,
|
||||
width: 200,
|
||||
height: 150,
|
||||
fit: 'contain'
|
||||
},
|
||||
data: {}
|
||||
};
|
||||
|
||||
// 验证图片节点数据
|
||||
if (!imageNode.image.url || imageNode.image.url.trim() === '') {
|
||||
if (!imageUrl || imageUrl.trim() === '') {
|
||||
console.error(`❌ 图片节点 ${index + 1} URL为空:`, imageNode);
|
||||
return;
|
||||
}
|
||||
|
||||
parentNode.children.push(imageNode);
|
||||
console.log(`✅ 成功创建图片节点: ${imageNode.topic} - ${imageUrl}`);
|
||||
console.log(`✅ 成功创建图片节点: ${image.alt || `图片 ${index + 1}`} - ${imageUrl}`);
|
||||
});
|
||||
|
||||
// 移除图片后的内容
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import nodeDraggable from './plugin/nodeDraggable'
|
|||
import operationHistory from './plugin/operationHistory'
|
||||
import toolBar from './plugin/toolBar'
|
||||
import selection from './plugin/selection'
|
||||
import { editTopic, createWrapper, createParent, createChildren, createTopic, findEle } from './utils/dom'
|
||||
import { editTopic, editTableNode, convertTableHTMLToMarkdown, createWrapper, createParent, createChildren, createTopic, findEle } from './utils/dom'
|
||||
import { getObjById, generateNewObj, fillParent } from './utils/index'
|
||||
import { layout } from './utils/layout'
|
||||
import { changeTheme } from './utils/theme'
|
||||
|
|
@ -15,6 +15,7 @@ import * as nodeOperation from './nodeOperation'
|
|||
import * as arrow from './arrow'
|
||||
import * as summary from './summary'
|
||||
import * as exportImage from './plugin/exportImage'
|
||||
import { renderMarkdownToHTML } from '../../../utils/markdownRenderer'
|
||||
|
||||
export type OperationMap = typeof nodeOperation
|
||||
export type Operations = keyof OperationMap
|
||||
|
|
@ -58,6 +59,9 @@ const methods = {
|
|||
layout,
|
||||
linkDiv,
|
||||
editTopic,
|
||||
editTableNode,
|
||||
convertTableHTMLToMarkdown,
|
||||
renderMarkdownToHTML,
|
||||
createWrapper,
|
||||
createParent,
|
||||
createChildren,
|
||||
|
|
|
|||
|
|
@ -84,7 +84,16 @@ export default function (mind: MindElixirInstance) {
|
|||
return
|
||||
}
|
||||
|
||||
// 如果没有图片,则进入编辑模式
|
||||
// 检查是否是表格节点
|
||||
if (topic.nodeObj?.dangerouslySetInnerHTML && topic.innerHTML.includes('<table')) {
|
||||
console.log('📊 双击表格节点,准备编辑:', topic.nodeObj.topic)
|
||||
if (mind.editable) {
|
||||
mind.beginEdit(target)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 如果没有图片或表格,则进入编辑模式
|
||||
if (mind.editable) {
|
||||
mind.beginEdit(target)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -303,7 +303,18 @@ export const moveNodeAfter = function (this: MindElixirInstance, from: Topic[],
|
|||
export const beginEdit = function (this: MindElixirInstance, el?: Topic) {
|
||||
const nodeEle = el || this.currentNode
|
||||
if (!nodeEle) return
|
||||
if (nodeEle.nodeObj.dangerouslySetInnerHTML) return
|
||||
|
||||
// 检查是否是表格节点
|
||||
if (nodeEle.nodeObj.dangerouslySetInnerHTML) {
|
||||
// 检查是否包含表格
|
||||
if (nodeEle.innerHTML.includes('<table')) {
|
||||
this.editTableNode(nodeEle)
|
||||
return
|
||||
}
|
||||
// 其他dangerouslySetInnerHTML节点暂时不支持编辑
|
||||
return
|
||||
}
|
||||
|
||||
this.editTopic(nodeEle)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,319 +1,608 @@
|
|||
import { LEFT } from '../const'
|
||||
import type { Topic, Wrapper, Parent, Children, Expander } from '../types/dom'
|
||||
import type { MindElixirInstance, NodeObj } from '../types/index'
|
||||
import { encodeHTML, getOffsetLT } from '../utils/index'
|
||||
import { layoutChildren } from './layout'
|
||||
// 移除imageProcessor引用,使用MindElixir原生image属性
|
||||
|
||||
|
||||
// DOM manipulation
|
||||
const $d = document
|
||||
export const findEle = function (this: MindElixirInstance, id: string, el?: HTMLElement) {
|
||||
const scope = this?.el ? this.el : el ? el : document
|
||||
const ele = scope.querySelector<Topic>(`[data-nodeid="me${id}"]`)
|
||||
if (!ele) throw new Error(`FindEle: Node ${id} not found, maybe it's collapsed.`)
|
||||
return ele
|
||||
}
|
||||
|
||||
export const shapeTpc = function (this: MindElixirInstance, tpc: Topic, nodeObj: NodeObj) {
|
||||
tpc.innerHTML = ''
|
||||
|
||||
if (nodeObj.style) {
|
||||
const style = nodeObj.style
|
||||
type KeyOfStyle = keyof typeof style
|
||||
for (const key in style) {
|
||||
tpc.style[key as KeyOfStyle] = style[key as KeyOfStyle]!
|
||||
}
|
||||
}
|
||||
|
||||
if (nodeObj.dangerouslySetInnerHTML) {
|
||||
// 清理HTML内容,移除重复的格式
|
||||
let cleanedHTML = nodeObj.dangerouslySetInnerHTML
|
||||
|
||||
// 检查是否包含表格,如果包含表格则不进行列表处理
|
||||
const hasTable = cleanedHTML.includes('<table') || cleanedHTML.includes('<td>') || cleanedHTML.includes('<th>')
|
||||
|
||||
if (!hasTable) {
|
||||
// 移除• 【这种重复格式
|
||||
cleanedHTML = cleanedHTML.replace(/•\s*【/g, '【')
|
||||
cleanedHTML = cleanedHTML.replace(/•\s*\[/g, '[')
|
||||
|
||||
// 移除其他可能的重复格式
|
||||
cleanedHTML = cleanedHTML.replace(/•\s*(/g, '(')
|
||||
cleanedHTML = cleanedHTML.replace(/•\s*\(/g, '(')
|
||||
|
||||
// 处理换行符和列表格式
|
||||
// 将换行符转换为<br>标签,但保持列表结构
|
||||
cleanedHTML = cleanedHTML
|
||||
// 处理列表项:将每个•开头的行转换为<li>
|
||||
.replace(/^(\s*)•\s*(.+)$/gm, '<li>$2</li>')
|
||||
// 将连续的<li>标签包装在<ul>中
|
||||
.replace(/(<li>.*<\/li>)/gs, (match) => {
|
||||
// 如果已经被<ul>包装,不重复包装
|
||||
if (cleanedHTML.includes('<ul>')) return match
|
||||
return `<ul>${match}</ul>`
|
||||
})
|
||||
// 将剩余的换行符转换为<br>
|
||||
.replace(/\n/g, '<br>')
|
||||
} else {
|
||||
// 对于表格内容,只移除重复格式,不进行列表处理
|
||||
cleanedHTML = cleanedHTML.replace(/•\s*【/g, '【')
|
||||
cleanedHTML = cleanedHTML.replace(/•\s*\[/g, '[')
|
||||
cleanedHTML = cleanedHTML.replace(/•\s*(/g, '(')
|
||||
cleanedHTML = cleanedHTML.replace(/•\s*\(/g, '(')
|
||||
}
|
||||
|
||||
tpc.innerHTML = cleanedHTML
|
||||
|
||||
// 检查是否包含图片,决定文本对齐方式
|
||||
const hasImages = cleanedHTML.includes('<img')
|
||||
const hasNodeImage = !!nodeObj.image
|
||||
const hasAnyImage = hasImages || hasNodeImage
|
||||
|
||||
// 统一使用左对齐样式,简化逻辑
|
||||
tpc.classList.add('no-image')
|
||||
tpc.classList.remove('has-image')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (nodeObj.image) {
|
||||
const img = nodeObj.image
|
||||
if (img.url && img.width && img.height) {
|
||||
const imgEl = $d.createElement('img')
|
||||
// Use imageProxy function if provided, otherwise use original URL
|
||||
imgEl.src = this.imageProxy ? this.imageProxy(img.url) : img.url
|
||||
imgEl.style.width = img.width + 'px'
|
||||
imgEl.style.height = img.height + 'px'
|
||||
if (img.fit) imgEl.style.objectFit = img.fit
|
||||
tpc.appendChild(imgEl)
|
||||
tpc.image = imgEl
|
||||
} else {
|
||||
console.warn('Image url/width/height are required')
|
||||
}
|
||||
} else if (tpc.image) {
|
||||
tpc.image = undefined
|
||||
}
|
||||
|
||||
{
|
||||
const textEl = $d.createElement('span')
|
||||
textEl.className = 'text'
|
||||
|
||||
// Check if markdown parser is provided and topic contains markdown syntax
|
||||
let content = ''
|
||||
if (this.markdown) {
|
||||
content = this.markdown(nodeObj.topic, nodeObj)
|
||||
} else {
|
||||
// 直接设置文本内容,图片通过MindElixir原生image属性处理
|
||||
content = nodeObj.topic || ''
|
||||
}
|
||||
|
||||
// 清理文本内容,移除重复的格式
|
||||
content = content.replace(/•\s*【/g, '【')
|
||||
content = content.replace(/•\s*\[/g, '[')
|
||||
content = content.replace(/•\s*(/g, '(')
|
||||
content = content.replace(/•\s*\(/g, '(')
|
||||
|
||||
textEl.innerHTML = content
|
||||
tpc.appendChild(textEl)
|
||||
tpc.text = textEl
|
||||
|
||||
// 检查是否有图片,决定文本对齐方式
|
||||
const hasNodeImage = !!nodeObj.image
|
||||
const hasImageInText = content.includes('<img')
|
||||
const hasAnyImage = hasNodeImage || hasImageInText
|
||||
|
||||
// 统一使用左对齐样式,简化逻辑
|
||||
tpc.classList.add('no-image')
|
||||
tpc.classList.remove('has-image')
|
||||
}
|
||||
|
||||
if (nodeObj.hyperLink) {
|
||||
const linkEl = $d.createElement('a')
|
||||
linkEl.className = 'hyper-link'
|
||||
linkEl.target = '_blank'
|
||||
linkEl.innerText = '🔗'
|
||||
linkEl.href = nodeObj.hyperLink
|
||||
tpc.appendChild(linkEl)
|
||||
tpc.link = linkEl
|
||||
} else if (tpc.link) {
|
||||
tpc.link = undefined
|
||||
}
|
||||
|
||||
if (nodeObj.icons && nodeObj.icons.length) {
|
||||
const iconsEl = $d.createElement('span')
|
||||
iconsEl.className = 'icons'
|
||||
iconsEl.innerHTML = nodeObj.icons.map(icon => `<span>${encodeHTML(icon)}</span>`).join('')
|
||||
tpc.appendChild(iconsEl)
|
||||
tpc.icons = iconsEl
|
||||
} else if (tpc.icons) {
|
||||
tpc.icons = undefined
|
||||
}
|
||||
|
||||
if (nodeObj.tags && nodeObj.tags.length) {
|
||||
const tagsEl = $d.createElement('div')
|
||||
tagsEl.className = 'tags'
|
||||
|
||||
nodeObj.tags.forEach(tag => {
|
||||
const span = $d.createElement('span')
|
||||
|
||||
if (typeof tag === 'string') {
|
||||
span.textContent = tag
|
||||
} else {
|
||||
span.textContent = tag.text
|
||||
if (tag.className) {
|
||||
span.className = tag.className
|
||||
}
|
||||
if (tag.style) {
|
||||
Object.assign(span.style, tag.style)
|
||||
}
|
||||
}
|
||||
|
||||
tagsEl.appendChild(span)
|
||||
})
|
||||
|
||||
tpc.appendChild(tagsEl)
|
||||
tpc.tags = tagsEl
|
||||
} else if (tpc.tags) {
|
||||
tpc.tags = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// everything start from `Wrapper`
|
||||
export const createWrapper = function (this: MindElixirInstance, nodeObj: NodeObj, omitChildren?: boolean) {
|
||||
const grp = $d.createElement('me-wrapper') as Wrapper
|
||||
const { p, tpc } = this.createParent(nodeObj)
|
||||
grp.appendChild(p)
|
||||
if (!omitChildren && nodeObj.children && nodeObj.children.length > 0) {
|
||||
const expander = createExpander(nodeObj.expanded)
|
||||
p.appendChild(expander)
|
||||
// tpc.expander = expander
|
||||
if (nodeObj.expanded !== false) {
|
||||
const children = layoutChildren(this, nodeObj.children)
|
||||
grp.appendChild(children)
|
||||
}
|
||||
}
|
||||
return { grp, top: p, tpc }
|
||||
}
|
||||
|
||||
export const createParent = function (this: MindElixirInstance, nodeObj: NodeObj) {
|
||||
const p = $d.createElement('me-parent') as Parent
|
||||
const tpc = this.createTopic(nodeObj)
|
||||
shapeTpc.call(this, tpc, nodeObj)
|
||||
p.appendChild(tpc)
|
||||
|
||||
return { p, tpc }
|
||||
}
|
||||
|
||||
|
||||
export const createChildren = function (this: MindElixirInstance, wrappers: Wrapper[]) {
|
||||
const children = $d.createElement('me-children') as Children
|
||||
children.append(...wrappers)
|
||||
return children
|
||||
}
|
||||
|
||||
export const createTopic = function (this: MindElixirInstance, nodeObj: NodeObj) {
|
||||
const topic = $d.createElement('me-tpc') as Topic
|
||||
topic.nodeObj = nodeObj
|
||||
topic.dataset.nodeid = 'me' + nodeObj.id
|
||||
topic.draggable = this.draggable
|
||||
return topic
|
||||
}
|
||||
|
||||
export function selectText(div: HTMLElement) {
|
||||
const range = $d.createRange()
|
||||
range.selectNodeContents(div)
|
||||
const getSelection = window.getSelection()
|
||||
if (getSelection) {
|
||||
getSelection.removeAllRanges()
|
||||
getSelection.addRange(range)
|
||||
}
|
||||
}
|
||||
|
||||
export const editTopic = function (this: MindElixirInstance, el: Topic) {
|
||||
console.time('editTopic')
|
||||
if (!el) return
|
||||
const div = $d.createElement('div')
|
||||
const node = el.nodeObj
|
||||
|
||||
// Get the original content from topic
|
||||
const originalContent = node.topic
|
||||
|
||||
el.appendChild(div)
|
||||
div.id = 'input-box'
|
||||
div.textContent = originalContent
|
||||
div.contentEditable = 'plaintext-only'
|
||||
div.spellcheck = false
|
||||
const style = getComputedStyle(el)
|
||||
div.style.cssText = `min-width:${el.offsetWidth - 8}px;
|
||||
color:${style.color};
|
||||
padding:${style.padding};
|
||||
margin:${style.margin};
|
||||
font:${style.font};
|
||||
background-color:${style.backgroundColor !== 'rgba(0, 0, 0, 0)' && style.backgroundColor};
|
||||
border-radius:${style.borderRadius};`
|
||||
if (this.direction === LEFT) div.style.right = '0'
|
||||
|
||||
selectText(div)
|
||||
|
||||
this.bus.fire('operation', {
|
||||
name: 'beginEdit',
|
||||
obj: el.nodeObj,
|
||||
})
|
||||
|
||||
div.addEventListener('keydown', e => {
|
||||
e.stopPropagation()
|
||||
const key = e.key
|
||||
|
||||
if (key === 'Enter' || key === 'Tab') {
|
||||
// keep wrap for shift enter
|
||||
if (e.shiftKey) return
|
||||
|
||||
e.preventDefault()
|
||||
div.blur()
|
||||
this.container.focus()
|
||||
}
|
||||
})
|
||||
|
||||
div.addEventListener('blur', () => {
|
||||
if (!div) return
|
||||
const inputContent = div.textContent?.trim() || ''
|
||||
|
||||
if (inputContent === '') {
|
||||
node.topic = originalContent
|
||||
} else {
|
||||
// Update topic content
|
||||
node.topic = inputContent
|
||||
|
||||
if (this.markdown) {
|
||||
el.text.innerHTML = this.markdown(node.topic, node)
|
||||
} else {
|
||||
// Plain text content
|
||||
el.text.textContent = inputContent
|
||||
}
|
||||
}
|
||||
|
||||
div.remove()
|
||||
|
||||
// 重新应用样式和清理逻辑
|
||||
shapeTpc.call(this, el, node)
|
||||
|
||||
if (inputContent === originalContent) return
|
||||
|
||||
this.linkDiv()
|
||||
this.bus.fire('operation', {
|
||||
name: 'finishEdit',
|
||||
obj: node,
|
||||
origin: originalContent,
|
||||
})
|
||||
})
|
||||
console.timeEnd('editTopic')
|
||||
}
|
||||
|
||||
export const createExpander = function (expanded: boolean | undefined): Expander {
|
||||
const expander = $d.createElement('me-epd') as Expander
|
||||
// if expanded is undefined, treat as expanded
|
||||
expander.expanded = expanded !== false
|
||||
expander.className = expanded !== false ? 'minus' : ''
|
||||
return expander
|
||||
}
|
||||
import { LEFT } from '../const'
|
||||
import type { Topic, Wrapper, Parent, Children, Expander } from '../types/dom'
|
||||
import type { MindElixirInstance, NodeObj } from '../types/index'
|
||||
import { encodeHTML, getOffsetLT } from '../utils/index'
|
||||
import { layoutChildren } from './layout'
|
||||
import { marked } from 'marked'
|
||||
// 移除imageProcessor引用,使用MindElixir原生image属性
|
||||
|
||||
|
||||
// DOM manipulation
|
||||
const $d = document
|
||||
export const findEle = function (this: MindElixirInstance, id: string, el?: HTMLElement) {
|
||||
const scope = this?.el ? this.el : el ? el : document
|
||||
const ele = scope.querySelector<Topic>(`[data-nodeid="me${id}"]`)
|
||||
if (!ele) throw new Error(`FindEle: Node ${id} not found, maybe it's collapsed.`)
|
||||
return ele
|
||||
}
|
||||
|
||||
export const shapeTpc = function (this: MindElixirInstance, tpc: Topic, nodeObj: NodeObj) {
|
||||
tpc.innerHTML = ''
|
||||
|
||||
if (nodeObj.style) {
|
||||
const style = nodeObj.style
|
||||
type KeyOfStyle = keyof typeof style
|
||||
for (const key in style) {
|
||||
tpc.style[key as KeyOfStyle] = style[key as KeyOfStyle]!
|
||||
}
|
||||
}
|
||||
|
||||
if (nodeObj.dangerouslySetInnerHTML) {
|
||||
// 清理HTML内容,移除重复的格式
|
||||
let cleanedHTML = nodeObj.dangerouslySetInnerHTML
|
||||
|
||||
// 检查是否包含表格,如果包含表格则不进行列表处理
|
||||
const hasTable = cleanedHTML.includes('<table') || cleanedHTML.includes('<td>') || cleanedHTML.includes('<th>')
|
||||
|
||||
if (!hasTable) {
|
||||
// 移除• 【这种重复格式
|
||||
cleanedHTML = cleanedHTML.replace(/•\s*【/g, '【')
|
||||
cleanedHTML = cleanedHTML.replace(/•\s*\[/g, '[')
|
||||
|
||||
// 移除其他可能的重复格式
|
||||
cleanedHTML = cleanedHTML.replace(/•\s*(/g, '(')
|
||||
cleanedHTML = cleanedHTML.replace(/•\s*\(/g, '(')
|
||||
|
||||
// 处理换行符和列表格式
|
||||
// 将换行符转换为<br>标签,但保持列表结构
|
||||
cleanedHTML = cleanedHTML
|
||||
// 处理列表项:将每个•开头的行转换为<li>
|
||||
.replace(/^(\s*)•\s*(.+)$/gm, '<li>$2</li>')
|
||||
// 将连续的<li>标签包装在<ul>中
|
||||
.replace(/(<li>.*<\/li>)/gs, (match) => {
|
||||
// 如果已经被<ul>包装,不重复包装
|
||||
if (cleanedHTML.includes('<ul>')) return match
|
||||
return `<ul>${match}</ul>`
|
||||
})
|
||||
// 将剩余的换行符转换为<br>
|
||||
.replace(/\n/g, '<br>')
|
||||
} else {
|
||||
// 对于表格内容,只移除重复格式,不进行列表处理
|
||||
cleanedHTML = cleanedHTML.replace(/•\s*【/g, '【')
|
||||
cleanedHTML = cleanedHTML.replace(/•\s*\[/g, '[')
|
||||
cleanedHTML = cleanedHTML.replace(/•\s*(/g, '(')
|
||||
cleanedHTML = cleanedHTML.replace(/•\s*\(/g, '(')
|
||||
}
|
||||
|
||||
tpc.innerHTML = cleanedHTML
|
||||
|
||||
// 检查是否包含图片,决定文本对齐方式
|
||||
const hasImages = cleanedHTML.includes('<img')
|
||||
const hasNodeImage = !!nodeObj.image
|
||||
const hasAnyImage = hasImages || hasNodeImage
|
||||
|
||||
// 统一使用左对齐样式,简化逻辑
|
||||
tpc.classList.add('no-image')
|
||||
tpc.classList.remove('has-image')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (nodeObj.image) {
|
||||
const img = nodeObj.image
|
||||
if (img.url && img.width && img.height) {
|
||||
const imgEl = $d.createElement('img')
|
||||
// Use imageProxy function if provided, otherwise use original URL
|
||||
imgEl.src = this.imageProxy ? this.imageProxy(img.url) : img.url
|
||||
imgEl.style.width = img.width + 'px'
|
||||
imgEl.style.height = img.height + 'px'
|
||||
if (img.fit) imgEl.style.objectFit = img.fit
|
||||
tpc.appendChild(imgEl)
|
||||
tpc.image = imgEl
|
||||
} else {
|
||||
console.warn('Image url/width/height are required')
|
||||
}
|
||||
} else if (tpc.image) {
|
||||
tpc.image = undefined
|
||||
}
|
||||
|
||||
{
|
||||
const textEl = $d.createElement('span')
|
||||
textEl.className = 'text'
|
||||
|
||||
// Check if markdown parser is provided and topic contains markdown syntax
|
||||
let content = ''
|
||||
if (this.markdown) {
|
||||
content = this.markdown(nodeObj.topic, nodeObj)
|
||||
} else {
|
||||
// 直接设置文本内容,图片通过MindElixir原生image属性处理
|
||||
content = nodeObj.topic || ''
|
||||
}
|
||||
|
||||
// 清理文本内容,移除重复的格式
|
||||
content = content.replace(/•\s*【/g, '【')
|
||||
content = content.replace(/•\s*\[/g, '[')
|
||||
content = content.replace(/•\s*(/g, '(')
|
||||
content = content.replace(/•\s*\(/g, '(')
|
||||
|
||||
textEl.innerHTML = content
|
||||
tpc.appendChild(textEl)
|
||||
tpc.text = textEl
|
||||
|
||||
// 检查是否有图片,决定文本对齐方式
|
||||
const hasNodeImage = !!nodeObj.image
|
||||
const hasImageInText = content.includes('<img')
|
||||
const hasAnyImage = hasNodeImage || hasImageInText
|
||||
|
||||
// 统一使用左对齐样式,简化逻辑
|
||||
tpc.classList.add('no-image')
|
||||
tpc.classList.remove('has-image')
|
||||
}
|
||||
|
||||
if (nodeObj.hyperLink) {
|
||||
const linkEl = $d.createElement('a')
|
||||
linkEl.className = 'hyper-link'
|
||||
linkEl.target = '_blank'
|
||||
linkEl.innerText = '🔗'
|
||||
linkEl.href = nodeObj.hyperLink
|
||||
tpc.appendChild(linkEl)
|
||||
tpc.link = linkEl
|
||||
} else if (tpc.link) {
|
||||
tpc.link = undefined
|
||||
}
|
||||
|
||||
if (nodeObj.icons && nodeObj.icons.length) {
|
||||
const iconsEl = $d.createElement('span')
|
||||
iconsEl.className = 'icons'
|
||||
iconsEl.innerHTML = nodeObj.icons.map(icon => `<span>${encodeHTML(icon)}</span>`).join('')
|
||||
tpc.appendChild(iconsEl)
|
||||
tpc.icons = iconsEl
|
||||
} else if (tpc.icons) {
|
||||
tpc.icons = undefined
|
||||
}
|
||||
|
||||
if (nodeObj.tags && nodeObj.tags.length) {
|
||||
const tagsEl = $d.createElement('div')
|
||||
tagsEl.className = 'tags'
|
||||
|
||||
nodeObj.tags.forEach(tag => {
|
||||
const span = $d.createElement('span')
|
||||
|
||||
if (typeof tag === 'string') {
|
||||
span.textContent = tag
|
||||
} else {
|
||||
span.textContent = tag.text
|
||||
if (tag.className) {
|
||||
span.className = tag.className
|
||||
}
|
||||
if (tag.style) {
|
||||
Object.assign(span.style, tag.style)
|
||||
}
|
||||
}
|
||||
|
||||
tagsEl.appendChild(span)
|
||||
})
|
||||
|
||||
tpc.appendChild(tagsEl)
|
||||
tpc.tags = tagsEl
|
||||
} else if (tpc.tags) {
|
||||
tpc.tags = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// everything start from `Wrapper`
|
||||
export const createWrapper = function (this: MindElixirInstance, nodeObj: NodeObj, omitChildren?: boolean) {
|
||||
const grp = $d.createElement('me-wrapper') as Wrapper
|
||||
const { p, tpc } = this.createParent(nodeObj)
|
||||
grp.appendChild(p)
|
||||
if (!omitChildren && nodeObj.children && nodeObj.children.length > 0) {
|
||||
const expander = createExpander(nodeObj.expanded)
|
||||
p.appendChild(expander)
|
||||
// tpc.expander = expander
|
||||
if (nodeObj.expanded !== false) {
|
||||
const children = layoutChildren(this, nodeObj.children)
|
||||
grp.appendChild(children)
|
||||
}
|
||||
}
|
||||
return { grp, top: p, tpc }
|
||||
}
|
||||
|
||||
export const createParent = function (this: MindElixirInstance, nodeObj: NodeObj) {
|
||||
const p = $d.createElement('me-parent') as Parent
|
||||
const tpc = this.createTopic(nodeObj)
|
||||
shapeTpc.call(this, tpc, nodeObj)
|
||||
p.appendChild(tpc)
|
||||
|
||||
return { p, tpc }
|
||||
}
|
||||
|
||||
|
||||
export const createChildren = function (this: MindElixirInstance, wrappers: Wrapper[]) {
|
||||
const children = $d.createElement('me-children') as Children
|
||||
children.append(...wrappers)
|
||||
return children
|
||||
}
|
||||
|
||||
export const createTopic = function (this: MindElixirInstance, nodeObj: NodeObj) {
|
||||
const topic = $d.createElement('me-tpc') as Topic
|
||||
topic.nodeObj = nodeObj
|
||||
topic.dataset.nodeid = 'me' + nodeObj.id
|
||||
topic.draggable = this.draggable
|
||||
return topic
|
||||
}
|
||||
|
||||
export function selectText(div: HTMLElement) {
|
||||
const range = $d.createRange()
|
||||
range.selectNodeContents(div)
|
||||
const getSelection = window.getSelection()
|
||||
if (getSelection) {
|
||||
getSelection.removeAllRanges()
|
||||
getSelection.addRange(range)
|
||||
}
|
||||
}
|
||||
|
||||
export const editTopic = function (this: MindElixirInstance, el: Topic) {
|
||||
console.time('editTopic')
|
||||
if (!el) return
|
||||
const div = $d.createElement('div')
|
||||
const node = el.nodeObj
|
||||
|
||||
// Get the original content from topic
|
||||
const originalContent = node.topic
|
||||
|
||||
el.appendChild(div)
|
||||
div.id = 'input-box'
|
||||
div.textContent = originalContent
|
||||
div.contentEditable = 'plaintext-only'
|
||||
div.spellcheck = false
|
||||
const style = getComputedStyle(el)
|
||||
div.style.cssText = `min-width:${el.offsetWidth - 8}px;
|
||||
color:${style.color};
|
||||
padding:${style.padding};
|
||||
margin:${style.margin};
|
||||
font:${style.font};
|
||||
background-color:${style.backgroundColor !== 'rgba(0, 0, 0, 0)' && style.backgroundColor};
|
||||
border-radius:${style.borderRadius};`
|
||||
if (this.direction === LEFT) div.style.right = '0'
|
||||
|
||||
selectText(div)
|
||||
|
||||
this.bus.fire('operation', {
|
||||
name: 'beginEdit',
|
||||
obj: el.nodeObj,
|
||||
})
|
||||
|
||||
div.addEventListener('keydown', e => {
|
||||
e.stopPropagation()
|
||||
const key = e.key
|
||||
|
||||
if (key === 'Enter' || key === 'Tab') {
|
||||
// keep wrap for shift enter
|
||||
if (e.shiftKey) return
|
||||
|
||||
e.preventDefault()
|
||||
div.blur()
|
||||
this.container.focus()
|
||||
}
|
||||
})
|
||||
|
||||
div.addEventListener('blur', () => {
|
||||
if (!div) return
|
||||
const inputContent = div.textContent?.trim() || ''
|
||||
|
||||
if (inputContent === '') {
|
||||
node.topic = originalContent
|
||||
} else {
|
||||
// Update topic content
|
||||
node.topic = inputContent
|
||||
|
||||
if (this.markdown) {
|
||||
el.text.innerHTML = this.markdown(node.topic, node)
|
||||
} else {
|
||||
// Plain text content
|
||||
el.text.textContent = inputContent
|
||||
}
|
||||
}
|
||||
|
||||
div.remove()
|
||||
|
||||
// 重新应用样式和清理逻辑
|
||||
shapeTpc.call(this, el, node)
|
||||
|
||||
if (inputContent === originalContent) return
|
||||
|
||||
this.linkDiv()
|
||||
this.bus.fire('operation', {
|
||||
name: 'finishEdit',
|
||||
obj: node,
|
||||
origin: originalContent,
|
||||
})
|
||||
})
|
||||
console.timeEnd('editTopic')
|
||||
}
|
||||
|
||||
export const createExpander = function (expanded: boolean | undefined): Expander {
|
||||
const expander = $d.createElement('me-epd') as Expander
|
||||
// if expanded is undefined, treat as expanded
|
||||
expander.expanded = expanded !== false
|
||||
expander.className = expanded !== false ? 'minus' : ''
|
||||
return expander
|
||||
}
|
||||
|
||||
// 表格编辑功能
|
||||
export const editTableNode = function (this: MindElixirInstance, el: Topic) {
|
||||
console.time('editTableNode')
|
||||
if (!el) return
|
||||
|
||||
const node = el.nodeObj
|
||||
const originalHTML = node.dangerouslySetInnerHTML || ''
|
||||
|
||||
// 创建遮罩层
|
||||
const overlay = $d.createElement('div')
|
||||
overlay.id = 'table-edit-overlay'
|
||||
overlay.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 9999;
|
||||
`
|
||||
|
||||
// 创建编辑容器 - 使用fixed定位,居中显示
|
||||
const editContainer = $d.createElement('div')
|
||||
editContainer.id = 'table-edit-container'
|
||||
editContainer.style.cssText = `
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 600px;
|
||||
min-height: 400px;
|
||||
max-height: 80vh;
|
||||
background: white;
|
||||
border: 2px solid #007bff;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
z-index: 10000;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`
|
||||
|
||||
// 创建标题
|
||||
const title = $d.createElement('div')
|
||||
title.textContent = '编辑表格(Markdown格式)'
|
||||
title.style.cssText = `
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
`
|
||||
|
||||
// 创建文本区域用于编辑表格markdown
|
||||
const textarea = $d.createElement('textarea')
|
||||
textarea.value = this.convertTableHTMLToMarkdown(originalHTML)
|
||||
textarea.style.cssText = `
|
||||
flex: 1;
|
||||
min-height: 300px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
resize: vertical;
|
||||
outline: none;
|
||||
background: #fafafa;
|
||||
color: #333;
|
||||
caret-color: #007bff;
|
||||
`
|
||||
textarea.setAttribute('spellcheck', 'false')
|
||||
textarea.setAttribute('autocomplete', 'off')
|
||||
textarea.setAttribute('autocorrect', 'off')
|
||||
textarea.setAttribute('autocapitalize', 'off')
|
||||
|
||||
// 创建按钮容器
|
||||
const buttonContainer = $d.createElement('div')
|
||||
buttonContainer.style.cssText = `
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
`
|
||||
|
||||
// 创建提示文字
|
||||
const hint = $d.createElement('span')
|
||||
hint.textContent = '提示: Ctrl+Enter保存, Esc取消'
|
||||
hint.style.cssText = `
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
`
|
||||
|
||||
// 创建按钮组
|
||||
const buttonGroup = $d.createElement('div')
|
||||
buttonGroup.style.cssText = `
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
// 创建取消按钮
|
||||
const cancelButton = $d.createElement('button')
|
||||
cancelButton.textContent = '取消'
|
||||
cancelButton.style.cssText = `
|
||||
padding: 8px 20px;
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: background 0.2s;
|
||||
`
|
||||
cancelButton.addEventListener('mouseenter', () => {
|
||||
cancelButton.style.background = '#5a6268'
|
||||
})
|
||||
cancelButton.addEventListener('mouseleave', () => {
|
||||
cancelButton.style.background = '#6c757d'
|
||||
})
|
||||
|
||||
// 创建保存按钮
|
||||
const saveButton = $d.createElement('button')
|
||||
saveButton.textContent = '保存'
|
||||
saveButton.style.cssText = `
|
||||
padding: 8px 20px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s;
|
||||
`
|
||||
saveButton.addEventListener('mouseenter', () => {
|
||||
saveButton.style.background = '#0056b3'
|
||||
})
|
||||
saveButton.addEventListener('mouseleave', () => {
|
||||
saveButton.style.background = '#007bff'
|
||||
})
|
||||
|
||||
buttonGroup.appendChild(cancelButton)
|
||||
buttonGroup.appendChild(saveButton)
|
||||
buttonContainer.appendChild(hint)
|
||||
buttonContainer.appendChild(buttonGroup)
|
||||
|
||||
editContainer.appendChild(title)
|
||||
editContainer.appendChild(textarea)
|
||||
editContainer.appendChild(buttonContainer)
|
||||
|
||||
// 添加到body(因为使用了fixed定位)
|
||||
$d.body.appendChild(overlay)
|
||||
$d.body.appendChild(editContainer)
|
||||
|
||||
// 点击遮罩层关闭编辑器
|
||||
overlay.addEventListener('click', () => {
|
||||
overlay.remove()
|
||||
editContainer.remove()
|
||||
})
|
||||
|
||||
// 延迟聚焦,确保DOM已渲染
|
||||
setTimeout(() => {
|
||||
textarea.focus()
|
||||
textarea.setSelectionRange(0, 0) // 光标移到开始
|
||||
console.log('📊 编辑器已聚焦,光标应该可见')
|
||||
}, 50)
|
||||
|
||||
// 保存功能
|
||||
const saveTable = () => {
|
||||
const newMarkdown = textarea.value.trim()
|
||||
|
||||
console.log('📊 保存表格,原始markdown:', newMarkdown)
|
||||
|
||||
if (newMarkdown === this.convertTableHTMLToMarkdown(originalHTML)) {
|
||||
// 没有变化,直接关闭
|
||||
console.log('📊 表格内容未变化,直接关闭')
|
||||
overlay.remove()
|
||||
editContainer.remove()
|
||||
return
|
||||
}
|
||||
|
||||
// 配置marked
|
||||
marked.setOptions({
|
||||
breaks: false,
|
||||
gfm: true,
|
||||
})
|
||||
|
||||
// 转换markdown为HTML (marked.parse返回Promise或string)
|
||||
let newHTML = marked.parse(newMarkdown) as string
|
||||
console.log('📊 转换后的HTML:', newHTML)
|
||||
|
||||
// 为表格添加样式类
|
||||
const styledHTML = newHTML.replace(/<table>/g, '<table class="markdown-table">')
|
||||
|
||||
console.log('📊 更新节点,新HTML长度:', styledHTML.length)
|
||||
|
||||
// 移除编辑容器和遮罩层
|
||||
overlay.remove()
|
||||
editContainer.remove()
|
||||
|
||||
// 更新节点数据
|
||||
node.dangerouslySetInnerHTML = styledHTML
|
||||
node.topic = newMarkdown // 同时保存markdown原文
|
||||
|
||||
console.log('📊 直接更新DOM内容')
|
||||
// 直接更新DOM,不调用shapeTpc(因为shapeTpc会清空innerHTML)
|
||||
el.innerHTML = styledHTML
|
||||
|
||||
// 添加样式类
|
||||
el.classList.add('no-image')
|
||||
el.classList.remove('has-image')
|
||||
|
||||
console.log('📊 触发操作事件')
|
||||
// 触发操作事件
|
||||
this.bus.fire('operation', {
|
||||
name: 'finishEditTable',
|
||||
obj: node,
|
||||
origin: originalHTML,
|
||||
})
|
||||
|
||||
console.log('📊 重新布局')
|
||||
this.linkDiv()
|
||||
}
|
||||
|
||||
// 取消功能
|
||||
const cancelEdit = () => {
|
||||
overlay.remove()
|
||||
editContainer.remove()
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
saveButton.addEventListener('click', saveTable)
|
||||
cancelButton.addEventListener('click', cancelEdit)
|
||||
|
||||
// 键盘事件
|
||||
textarea.addEventListener('keydown', (e) => {
|
||||
e.stopPropagation()
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
cancelEdit()
|
||||
} else if (e.key === 'Enter' && e.ctrlKey) {
|
||||
e.preventDefault()
|
||||
saveTable()
|
||||
}
|
||||
})
|
||||
|
||||
console.timeEnd('editTableNode')
|
||||
}
|
||||
|
||||
// 将表格HTML转换为markdown
|
||||
export const convertTableHTMLToMarkdown = function (this: MindElixirInstance, html: string): string {
|
||||
// 创建临时DOM元素
|
||||
const tempDiv = $d.createElement('div')
|
||||
tempDiv.innerHTML = html
|
||||
|
||||
const table = tempDiv.querySelector('table')
|
||||
if (!table) return html
|
||||
|
||||
let markdown = ''
|
||||
|
||||
// 处理表头
|
||||
const thead = table.querySelector('thead')
|
||||
if (thead) {
|
||||
const headerRow = thead.querySelector('tr')
|
||||
if (headerRow) {
|
||||
const headers = Array.from(headerRow.querySelectorAll('th')).map(th => th.textContent?.trim() || '')
|
||||
markdown += '| ' + headers.join(' | ') + ' |\n'
|
||||
markdown += '| ' + headers.map(() => '---').join(' | ') + ' |\n'
|
||||
}
|
||||
}
|
||||
|
||||
// 处理表体
|
||||
const tbody = table.querySelector('tbody')
|
||||
if (tbody) {
|
||||
const rows = tbody.querySelectorAll('tr')
|
||||
rows.forEach(row => {
|
||||
const cells = Array.from(row.querySelectorAll('td')).map(td => td.textContent?.trim() || '')
|
||||
markdown += '| ' + cells.join(' | ') + ' |\n'
|
||||
})
|
||||
}
|
||||
|
||||
return markdown.trim()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ type NodeOperation =
|
|||
obj: NodeObj
|
||||
origin: string
|
||||
}
|
||||
| {
|
||||
name: 'finishEditTable'
|
||||
obj: NodeObj
|
||||
origin: string
|
||||
}
|
||||
| {
|
||||
name: 'moveNodeAfter' | 'moveNodeBefore' | 'moveNodeIn'
|
||||
objs: NodeObj[]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
export declare function renderMarkdownToHTML(markdown: string): string;
|
||||
export declare function hasMarkdownSyntax(content: string): boolean;
|
||||
export declare function preprocessMarkdown(markdown: string): string;
|
||||
export declare function postprocessHTML(html: string): string;
|
||||
|
|
@ -14,9 +14,26 @@ import 'prismjs/components/prism-python';
|
|||
import 'prismjs/components/prism-sql';
|
||||
import 'katex/dist/katex.min.css';
|
||||
|
||||
// 自定义渲染器(移除图片处理,使用MindElixir原生image属性)
|
||||
// 自定义渲染器(支持图片渲染)
|
||||
const renderer = new marked.Renderer();
|
||||
|
||||
// 自定义图片渲染器
|
||||
renderer.image = function(href, title, text) {
|
||||
// 处理图片URL,确保能正确显示
|
||||
let processedUrl = href;
|
||||
if (href.includes('cdn-mineru.openxlab.org.cn')) {
|
||||
// 将外部CDN URL转换为代理URL
|
||||
const urlPath = href.replace('https://cdn-mineru.openxlab.org.cn', '');
|
||||
processedUrl = `/proxy-image${urlPath}`;
|
||||
}
|
||||
|
||||
// 生成图片HTML
|
||||
const altText = text || '图片';
|
||||
const titleAttr = title ? ` title="${title}"` : '';
|
||||
|
||||
return `<img src="${processedUrl}" alt="${altText}"${titleAttr} class="markdown-image" style="max-width: 200px; max-height: 150px; object-fit: contain; display: block; margin: 4px auto; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);" />`;
|
||||
};
|
||||
|
||||
// 配置marked选项
|
||||
marked.setOptions({
|
||||
breaks: false, // 禁用breaks,避免在表格中产生<br>标签导致HTML不匹配
|
||||
|
|
@ -409,6 +426,26 @@ const addMarkdownStyles = (container) => {
|
|||
font-family: monospace;
|
||||
}
|
||||
|
||||
.markdown-image {
|
||||
max-width: 200px !important;
|
||||
max-height: 150px !important;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
object-fit: contain !important;
|
||||
display: block !important;
|
||||
margin: 4px auto !important;
|
||||
border-radius: 4px !important;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;
|
||||
background-color: #f5f5f5 !important;
|
||||
cursor: pointer !important;
|
||||
transition: transform 0.2s ease !important;
|
||||
}
|
||||
|
||||
.markdown-image:hover {
|
||||
transform: scale(1.02) !important;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15) !important;
|
||||
}
|
||||
|
||||
.markdown-error {
|
||||
color: #dc3545;
|
||||
background: #f8d7da;
|
||||
|
|
|
|||
Loading…
Reference in New Issue