Skip to content

Commit b8501f0

Browse files
author
ws-wangjg
committed
feat: pro mdx editor
1 parent f09cc69 commit b8501f0

18 files changed

Lines changed: 1903 additions & 5 deletions

File tree

package-lock.json

Lines changed: 892 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,13 +327,27 @@
327327
"@emotion/is-prop-valid": "^1.4.0",
328328
"@loadable/component": "^5.16.7",
329329
"@number-flow/react": "^0.5.10",
330+
"@phosphor-icons/react": "^2.1.10",
330331
"@react-spring/core": "^10.0.3",
331332
"@react-spring/web": "^10.0.3",
332333
"@react-three/drei": "^10.7.7",
333334
"@react-three/fiber": "^9.5.0",
334335
"@sentry/react": "^10.33.0",
335336
"@sentry/tracing": "^7.120.4",
336337
"@smakss/react-scroll-direction": "^4.2.0",
338+
"@tiptap/extension-image": "^3.15.3",
339+
"@tiptap/extension-link": "^3.15.3",
340+
"@tiptap/extension-placeholder": "^3.15.3",
341+
"@tiptap/extension-table": "^3.15.3",
342+
"@tiptap/extension-table-cell": "^3.15.3",
343+
"@tiptap/extension-table-header": "^3.15.3",
344+
"@tiptap/extension-table-row": "^3.15.3",
345+
"@tiptap/extension-task-item": "^3.15.3",
346+
"@tiptap/extension-task-list": "^3.15.3",
347+
"@tiptap/extension-text-align": "^3.15.3",
348+
"@tiptap/extension-underline": "^3.15.3",
349+
"@tiptap/react": "^3.15.3",
350+
"@tiptap/starter-kit": "^3.15.3",
337351
"@tsparticles/engine": "^3.9.1",
338352
"@tsparticles/react": "^3.0.0",
339353
"@tsparticles/slim": "^3.9.1",
@@ -370,6 +384,7 @@
370384
"lodash-es": "^4.17.22",
371385
"lucide-react": "^0.562.0",
372386
"maath": "^0.10.8",
387+
"marked": "^17.0.1",
373388
"markmap-common": "^0.18.9",
374389
"markmap-lib": "^0.18.12",
375390
"markmap-view": "^0.18.12",
@@ -408,6 +423,7 @@
408423
"resize-observer-polyfill": "^1.5.1",
409424
"screenfull": "^6.0.2",
410425
"three": "^0.182.0",
426+
"turndown": "^7.2.2",
411427
"use-debounce": "^10.1.0",
412428
"zustand": "^5.0.10"
413429
},
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import React, { memo, useEffect, useRef } from 'react'
2+
import { EditorContent } from '@tiptap/react'
3+
import styles from './index.module.less'
4+
5+
function EditorCore({ editor, onStatsUpdate }) {
6+
const containerRef = useRef(null)
7+
8+
useEffect(() => {
9+
if (!editor) return
10+
const updateStats = () => {
11+
const text = editor.getText()
12+
const chars = text.length
13+
const words = text
14+
.trim()
15+
.split(/\s+/)
16+
.filter((w) => w.length > 0).length
17+
const { from } = editor.state.selection
18+
const state = editor.state
19+
const textBefore = state.doc.textBetween(0, from, '\n')
20+
const lines = textBefore.split('\n')
21+
const line = lines.length
22+
const col = lines[lines.length - 1].length + 1
23+
onStatsUpdate({ cursorPos: `行 ${line}, 列 ${col}`, stats: `${chars} 字符 / ${words} 词` })
24+
}
25+
26+
updateStats()
27+
editor.on('selectionUpdate', updateStats)
28+
editor.on('update', updateStats)
29+
return () => {
30+
editor.off('selectionUpdate', updateStats)
31+
editor.off('update', updateStats)
32+
}
33+
}, [editor, onStatsUpdate])
34+
35+
useEffect(() => {
36+
const container = containerRef.current
37+
if (!container || !editor) return
38+
const handleClick = (e) => {
39+
if (e.target.classList.contains('ProseMirror')) {
40+
const contentHeight = editor.view.dom.scrollHeight
41+
const clickY = e.offsetY
42+
if (clickY > contentHeight - 50) {
43+
e.preventDefault()
44+
const estimatedLineHeight = 45
45+
const distance = clickY - contentHeight
46+
const linesToAdd = Math.floor(distance / estimatedLineHeight)
47+
let finalLines = Math.min(Math.max(linesToAdd, 1), 50)
48+
if (finalLines > 0) {
49+
const newParagraphs = Array(finalLines).fill('<p></p>').join('')
50+
editor.chain().focus('end').insertContent(newParagraphs).run()
51+
}
52+
}
53+
}
54+
}
55+
container.addEventListener('click', handleClick)
56+
return () => container.removeEventListener('click', handleClick)
57+
}, [editor])
58+
59+
return (
60+
<div className={`${styles.mdxEditorCol} mdxEditorCol`} ref={containerRef}>
61+
<EditorContent editor={editor} />
62+
</div>
63+
)
64+
}
65+
66+
export default memo(EditorCore)
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
@import '../../styles/global.less';
2+
3+
.mdxEditorCol {
4+
flex: 1;
5+
height: 100%;
6+
overflow-y: auto;
7+
padding: 40px;
8+
background: var(--mdx-bg-surface);
9+
transition: background 0.3s;
10+
}
11+
12+
// 严谨的 Tiptap 样式穿透,只在 .mdxEditorCol 内生效
13+
.mdxEditorCol :global(.ProseMirror) {
14+
outline: none;
15+
min-height: 100%;
16+
max-width: 840px;
17+
margin: 0 auto;
18+
color: var(--mdx-text-main);
19+
line-height: 1.75;
20+
font-size: 16px;
21+
padding-bottom: 50vh;
22+
cursor: text;
23+
24+
p.is-editor-empty:first-child::before {
25+
content: attr(data-placeholder);
26+
color: var(--mdx-text-muted);
27+
pointer-events: none;
28+
float: left;
29+
height: 0;
30+
font-style: italic;
31+
}
32+
33+
h1, h2, h3, h4 {
34+
font-weight: 700;
35+
line-height: 1.3;
36+
margin-top: 1.5em;
37+
margin-bottom: 0.5em;
38+
}
39+
40+
h1 {
41+
font-size: 2.25em;
42+
border-bottom: 1px solid var(--mdx-border);
43+
padding-bottom: 0.3em;
44+
}
45+
46+
h2 { font-size: 1.75em; }
47+
48+
ul, ol { padding-left: 1.5em; margin-bottom: 1em; }
49+
li { margin: 0.25em 0; }
50+
51+
ul[data-type="taskList"] {
52+
list-style: none;
53+
padding: 0;
54+
55+
li {
56+
display: flex;
57+
align-items: center;
58+
gap: 8px;
59+
60+
> label {
61+
flex: 0 0 auto;
62+
margin: 0;
63+
cursor: pointer;
64+
}
65+
66+
> div {
67+
flex: 1 1 auto;
68+
line-height: 1.75;
69+
}
70+
}
71+
}
72+
73+
pre {
74+
background: var(--mdx-code-bg);
75+
color: var(--mdx-code-text);
76+
padding: 1rem;
77+
border-radius: 8px;
78+
overflow-x: auto;
79+
margin: 1.5rem 0;
80+
font-family: monospace;
81+
font-size: 0.9em;
82+
}
83+
84+
code {
85+
background: rgba(0,0,0,0.05);
86+
padding: 0.2em 0.4em;
87+
border-radius: 4px;
88+
font-family: monospace;
89+
font-size: 0.85em;
90+
color: #ef4444;
91+
}
92+
93+
pre code {
94+
background: transparent;
95+
color: inherit;
96+
padding: 0;
97+
}
98+
99+
blockquote {
100+
border-left: 4px solid var(--mdx-primary);
101+
padding-left: 1rem;
102+
font-style: italic;
103+
color: var(--mdx-text-muted);
104+
margin: 1.5em 0;
105+
}
106+
107+
img {
108+
max-width: 100%;
109+
height: auto;
110+
border-radius: 8px;
111+
margin: 1.5rem 0;
112+
display: block;
113+
box-shadow: var(--mdx-shadow-md);
114+
115+
&.ProseMirror-selectednode {
116+
outline: 3px solid var(--mdx-primary);
117+
}
118+
}
119+
120+
a {
121+
color: var(--mdx-primary);
122+
text-decoration: underline;
123+
cursor: pointer;
124+
}
125+
126+
table {
127+
border-collapse: collapse;
128+
table-layout: fixed;
129+
width: 100%;
130+
margin: 1.5em 0;
131+
overflow: hidden;
132+
}
133+
134+
td, th {
135+
min-width: 1em;
136+
border: 1px solid var(--mdx-border);
137+
padding: 6px 10px;
138+
vertical-align: top;
139+
box-sizing: border-box;
140+
background-color: var(--mdx-bg-surface);
141+
}
142+
143+
th {
144+
font-weight: 700;
145+
text-align: left;
146+
background-color: var(--mdx-bg-body);
147+
}
148+
149+
.selectedCell:after {
150+
z-index: 2;
151+
position: absolute;
152+
content: "";
153+
left: 0; right: 0; top: 0; bottom: 0;
154+
background: rgba(200, 200, 255, 0.4);
155+
pointer-events: none;
156+
}
157+
158+
.column-resize-handle {
159+
position: absolute;
160+
right: -2px;
161+
top: 0;
162+
bottom: 0;
163+
width: 4px;
164+
z-index: 20;
165+
background-color: #adf;
166+
pointer-events: none;
167+
}
168+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React, { memo } from 'react'
2+
import styles from './index.module.less'
3+
4+
function Footer({ cursorPos, stats }) {
5+
return (
6+
<footer className={`${styles.mdxFooter} mdxFooter`}>
7+
<span>{cursorPos}</span>
8+
<span>{stats}</span>
9+
</footer>
10+
)
11+
}
12+
13+
export default memo(Footer)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
@import '../../styles/global.less';
2+
3+
.mdxFooter,
4+
:global(.mdxFooter) {
5+
height: 28px;
6+
border-top: 1px solid var(--mdx-border);
7+
background: var(--mdx-bg-surface);
8+
display: flex;
9+
align-items: center;
10+
justify-content: space-between;
11+
padding: 0 15px;
12+
font-size: 11px;
13+
color: var(--mdx-text-muted);
14+
flex-shrink: 0;
15+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import React, { useCallback, useEffect, useState } from 'react'
2+
import { Download, Copy, Moon, Sun, PenTool } from 'lucide-react'
3+
import styles from './index.module.less'
4+
5+
export default function Header({ defaultTheme = 'light', onThemeChange, onCopy, onDownload }) {
6+
const [theme, setTheme] = useState(defaultTheme)
7+
8+
useEffect(() => {
9+
onThemeChange?.(theme)
10+
}, [theme, onThemeChange])
11+
12+
const handleToggleTheme = useCallback(() => {
13+
setTheme((t) => (t === 'light' ? 'dark' : 'light'))
14+
}, [])
15+
16+
return (
17+
<header className={styles.mdxHeader}>
18+
<div className={styles.mdxBrand}>
19+
<PenTool className={styles.mdxLogoIcon} />
20+
<span>Pro MDX Editor</span>
21+
</div>
22+
<div className={styles.mdxHeaderActions}>
23+
<button className={styles.mdxIconBtn} onClick={handleToggleTheme} title="切换主题">
24+
{theme === 'dark' ? <Sun size={18} /> : <Moon size={18} />}
25+
</button>
26+
<button className={styles.mdxIconBtn} onClick={onDownload} title="下载文件">
27+
<Download size={18} />
28+
</button>
29+
<button className={`${styles.mdxIconBtn} ${styles.primary}`} onClick={onCopy} title="复制 Markdown">
30+
<Copy size={18} />
31+
</button>
32+
</div>
33+
</header>
34+
)
35+
}

0 commit comments

Comments
 (0)