Skip to content

Commit ad44ae5

Browse files
author
ws-wangjg
committed
feat: mdx editor
1 parent a4962ba commit ad44ae5

9 files changed

Lines changed: 105 additions & 82 deletions

File tree

src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,4 @@ export { default as Watermark } from './stateless/Watermark'
151151
export { default as WordRotate } from './stateless/WordRotate'
152152
export { default as ScrollLayout } from './stateless/ScrollLayout'
153153
export { default as SliderCaptcha } from './stateless/SliderCaptcha'
154+
export { default as MdxEditor } from './stateless/MdxEditor'

src/components/stateless/MdxEditor/components/Header/index.jsx

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,22 @@
1-
import React, { useCallback, useEffect, useState } from 'react'
1+
import React from 'react'
22
import { Download, Copy, Moon, Sun, PenTool } from 'lucide-react'
33
import styles from './index.module.less'
44

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-
5+
export default function Header({ theme, onThemeChange, onCopy, onDownload }) {
166
return (
177
<header className={styles.mdxHeader}>
188
<div className={styles.mdxBrand}>
199
<PenTool className={styles.mdxLogoIcon} />
2010
<span>Pro MDX Editor</span>
2111
</div>
2212
<div className={styles.mdxHeaderActions}>
23-
<button className={styles.mdxIconBtn} onClick={handleToggleTheme} title="切换主题">
13+
<button className={styles.mdxIconBtn} onClick={onThemeChange} title="切换主题">
2414
{theme === 'dark' ? <Sun size={18} /> : <Moon size={18} />}
2515
</button>
2616
<button className={styles.mdxIconBtn} onClick={onDownload} title="下载文件">
2717
<Download size={18} />
2818
</button>
29-
<button className={`${styles.mdxIconBtn} ${styles.primary}`} onClick={onCopy} title="复制 Markdown">
19+
<button className={styles.mdxCopyBtn} onClick={onCopy} title="复制 Markdown">
3020
<Copy size={18} />
3121
</button>
3222
</div>

src/components/stateless/MdxEditor/components/Header/index.module.less

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,23 @@
5050
background: var(--mdx-bg-body);
5151
color: var(--mdx-text-main);
5252
}
53+
}
5354

54-
&.primary {
55-
background: var(--mdx-primary);
56-
color: white;
55+
.mdxCopyBtn {
56+
background: var(--mdx-primary);
57+
border: none;
58+
color: white;
59+
width: 36px;
60+
height: 36px;
61+
border-radius: 6px;
62+
cursor: pointer;
63+
display: flex;
64+
align-items: center;
65+
justify-content: center;
66+
font-size: 1.2rem;
67+
transition: all 0.2s;
5768

58-
&:hover {
59-
background: var(--mdx-primary-hover);
60-
}
69+
&:hover {
70+
background: var(--mdx-primary-hover);
6171
}
6272
}

src/components/stateless/MdxEditor/components/Toolbar/index.jsx

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { memo, useEffect, useMemo, useReducer, useRef } from 'react'
1+
import React, { useMemo, useRef } from 'react'
22
import {
33
Undo2,
44
Redo2,
@@ -25,23 +25,13 @@ import {
2525
} from 'lucide-react'
2626
import styles from './index.module.less'
2727

28-
function Toolbar({ editor, viewMode, onChangeViewMode }) {
28+
// editorKey 参数用于触发重新渲染,确保工具栏高亮状态更新
29+
function Toolbar({ editor, viewMode, onChangeViewMode, editorKey }) {
2930
const fileInputRef = useRef(null)
3031
const iconSize = 18
31-
const [, forceRerender] = useReducer((x) => x + 1, 0)
3232

33-
useEffect(() => {
34-
if (!editor) return
35-
36-
const rerender = () => forceRerender()
37-
editor.on('selectionUpdate', rerender)
38-
editor.on('transaction', rerender)
39-
40-
return () => {
41-
editor.off('selectionUpdate', rerender)
42-
editor.off('transaction', rerender)
43-
}
44-
}, [editor])
33+
// editorKey 变化时强制重新计算
34+
void editorKey
4535

4636
const handleAction = (cmd, opt) => {
4737
if (!editor) return
@@ -114,16 +104,26 @@ function Toolbar({ editor, viewMode, onChangeViewMode }) {
114104

115105
if (!editor) return null
116106

117-
const isBtnActive = (cmd, opt) => {
118-
if (cmd === 'heading') {
119-
const level = parseInt(opt, 10)
120-
if (!isNaN(level)) return editor.isActive('heading', { level })
121-
}
122-
if (cmd === 'textAlign') {
123-
if (opt) return editor.isActive('textAlign', { textAlign: opt })
107+
// 检查按钮是否处于激活状态
108+
const checkActive = (cmd, opt) => {
109+
if (!editor || !editor.isActive) return false
110+
try {
111+
if (cmd === 'heading') {
112+
const level = Number(opt)
113+
// 必须严格检查当前是否是指定级别的 heading
114+
return level >= 1 && level <= 6 && editor.isActive('heading', { level })
115+
}
116+
if (cmd === 'textAlign') {
117+
return opt ? editor.isActive({ textAlign: opt }) : false
118+
}
119+
if (cmd === 'link') {
120+
return editor.isActive('link')
121+
}
122+
// 对于其他命令,直接检查
123+
return editor.isActive(cmd)
124+
} catch {
125+
return false
124126
}
125-
if (cmd === 'link') return editor.isActive('link')
126-
return editor.isActive(cmd)
127127
}
128128

129129
return (
@@ -151,7 +151,7 @@ function Toolbar({ editor, viewMode, onChangeViewMode }) {
151151
if (btn.type === 'separator') {
152152
return <div key={`sep-${idx}`} className={styles.mdxSeparator} />
153153
}
154-
const isActive = isBtnActive(btn.cmd, btn.opt)
154+
const isActive = checkActive(btn.cmd, btn.opt)
155155
return (
156156
<button
157157
key={`${btn.cmd}-${idx}`}
@@ -195,4 +195,4 @@ function Toolbar({ editor, viewMode, onChangeViewMode }) {
195195
)
196196
}
197197

198-
export default memo(Toolbar)
198+
export default Toolbar

src/components/stateless/MdxEditor/components/Toolbar/index.module.less

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,20 +78,20 @@
7878
align-items: center;
7979
justify-content: center;
8080
font-size: 1rem;
81-
transition: all 0.2s;
81+
transition: all 0.15s ease;
8282
position: relative;
8383

8484
&:hover {
8585
background: var(--mdx-bg-body);
8686
color: var(--mdx-text-main);
8787
border-color: var(--mdx-border);
8888
}
89+
}
8990

90-
&.active {
91-
background: rgba(37, 99, 235, 0.1);
92-
color: var(--mdx-primary);
93-
border-color: rgba(37, 99, 235, 0.2);
94-
}
91+
.mdxToolBtn.active {
92+
background: var(--mdx-active-bg, rgba(37, 99, 235, 0.1));
93+
color: var(--mdx-primary);
94+
border-color: var(--mdx-active-border, rgba(37, 99, 235, 0.2));
9595
}
9696

9797
.mdxFileInput {

src/components/stateless/MdxEditor/index.jsx

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ export default function ProMdxEditor() {
3232
const [markdown, setMarkdown] = useState(initialMarkdown)
3333
const [stats, setStats] = useState({ cursorPos: '行 1, 列 1', stats: '0 字符 / 0 词' })
3434
const [toast, setToast] = useState(null)
35-
const appRef = useRef(null)
35+
const [theme, setTheme] = useState('light')
36+
const [editorKey, setEditorKey] = useState(0) // 用于强制刷新工具栏
3637
const lastMarkdownFromEditorRef = useRef(markdown)
3738
const toastTimerRef = useRef(null)
38-
const defaultTheme = 'light'
3939

4040
const extensions = useMemo(() => {
4141
return [
@@ -63,6 +63,14 @@ export default function ProMdxEditor() {
6363
lastMarkdownFromEditorRef.current = md
6464
setMarkdown(md)
6565
},
66+
onSelectionUpdate: () => {
67+
// 触发工具栏更新
68+
setEditorKey((k) => k + 1)
69+
},
70+
onTransaction: () => {
71+
// 触发工具栏更新
72+
setEditorKey((k) => k + 1)
73+
},
6674
})
6775

6876
useEffect(() => {
@@ -80,10 +88,8 @@ export default function ProMdxEditor() {
8088
toastTimerRef.current = window.setTimeout(() => setToast(null), 2000)
8189
}, [])
8290

83-
const handleThemeChange = useCallback((nextTheme) => {
84-
const el = appRef.current
85-
if (!el) return
86-
el.setAttribute('data-mdx-theme', nextTheme)
91+
const handleThemeChange = useCallback(() => {
92+
setTheme((t) => (t === 'light' ? 'dark' : 'light'))
8793
}, [])
8894

8995
useEffect(() => {
@@ -120,6 +126,7 @@ export default function ProMdxEditor() {
120126
}
121127
}
122128
}, [markdown, showToast])
129+
123130
const handleDownload = () => {
124131
const blob = new Blob([markdown], { type: 'text/markdown' })
125132
const a = document.createElement('a')
@@ -129,16 +136,12 @@ export default function ProMdxEditor() {
129136
}
130137

131138
const viewClass = viewMode === 'split' ? styles.viewSplit : viewMode === 'code' ? styles.viewCode : ''
139+
const themeClass = theme === 'dark' ? styles.themeDark : ''
132140

133141
return (
134-
<div ref={appRef} className={`${styles.mdxApp} ${viewClass}`} data-mdx-theme={defaultTheme}>
135-
<Header
136-
defaultTheme={defaultTheme}
137-
onThemeChange={handleThemeChange}
138-
onCopy={handleCopy}
139-
onDownload={handleDownload}
140-
/>
141-
<Toolbar editor={editor} viewMode={viewMode} onChangeViewMode={setViewMode} />
142+
<div className={`${styles.mdxApp} ${viewClass} ${themeClass}`}>
143+
<Header theme={theme} onThemeChange={handleThemeChange} onCopy={handleCopy} onDownload={handleDownload} />
144+
<Toolbar editor={editor} viewMode={viewMode} onChangeViewMode={setViewMode} editorKey={editorKey} />
142145
<main className={styles.mdxMainContent}>
143146
<EditorCore editor={editor} onStatsUpdate={setStats} />
144147
{viewMode !== 'normal' && <SourceView markdown={markdown} onChange={handleMarkdownChange} />}

src/components/stateless/MdxEditor/index.module.less

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
@import './styles/global.less';
22

33
.mdxApp {
4+
--mdx-bg-body: #f8fafc;
5+
--mdx-bg-surface: #ffffff;
6+
--mdx-bg-toolbar: #ffffff;
7+
--mdx-border: #e2e8f0;
8+
--mdx-text-main: #0f172a;
9+
--mdx-text-muted: #64748b;
10+
--mdx-primary: #2563eb;
11+
--mdx-primary-hover: #1d4ed8;
12+
--mdx-code-bg: #1e293b;
13+
--mdx-code-text: #e2e8f0;
14+
--mdx-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
15+
--mdx-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
16+
--mdx-active-bg: rgba(37, 99, 235, 0.1);
17+
--mdx-active-border: rgba(37, 99, 235, 0.2);
18+
419
font-family: inherit;
520
background: var(--mdx-bg-body);
621
color: var(--mdx-text-main);
@@ -10,6 +25,20 @@
1025
position: relative;
1126
overflow: hidden;
1227
transition: background 0.3s, color 0.3s;
28+
29+
&.themeDark {
30+
--mdx-bg-body: #0f172a;
31+
--mdx-bg-surface: #1e293b;
32+
--mdx-bg-toolbar: #1e293b;
33+
--mdx-border: #334155;
34+
--mdx-text-main: #f1f5f9;
35+
--mdx-text-muted: #94a3b8;
36+
--mdx-primary: #3b82f6;
37+
--mdx-primary-hover: #60a5fa;
38+
--mdx-code-bg: #020617;
39+
--mdx-active-bg: rgba(59, 130, 246, 0.15);
40+
--mdx-active-border: rgba(59, 130, 246, 0.3);
41+
}
1342
}
1443

1544
.mdxMainContent {

src/components/stateless/MdxEditor/styles/global.less

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
// === MDX 全局变量 (加前缀以防冲突) ===
1+
// === MDX Editor 全局 CSS 变量 ===
2+
3+
// 默认浅色主题
24
:root {
35
--mdx-bg-body: #f8fafc;
46
--mdx-bg-surface: #ffffff;
@@ -12,25 +14,13 @@
1214
--mdx-code-text: #e2e8f0;
1315
--mdx-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
1416
--mdx-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
15-
}
16-
17-
// 深色模式变量
18-
[data-mdx-theme="dark"] {
19-
--mdx-bg-body: #0f172a;
20-
--mdx-bg-surface: #1e293b;
21-
--mdx-bg-toolbar: #1e293b;
22-
--mdx-border: #334155;
23-
--mdx-text-main: #f1f5f9;
24-
--mdx-text-muted: #94a3b8;
25-
--mdx-primary: #3b82f6;
26-
--mdx-primary-hover: #60a5fa;
27-
--mdx-code-bg: #020617;
17+
--mdx-active-bg: rgba(37, 99, 235, 0.1);
18+
--mdx-active-border: rgba(37, 99, 235, 0.2);
2819
}
2920

3021
// === 全局重置 ===
3122
* {
3223
box-sizing: border-box;
33-
outline: none;
3424
}
3525

3626
body {

src/components/stateless/MdxEditor/utils/markdownHelper.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ turndownService.addRule('tasklist', {
2121
// 表格规则 (基础支持)
2222
turndownService.addRule('table', {
2323
filter: 'table',
24-
replacement: function (content, node) {
24+
replacement: function (content) {
2525
return content
2626
},
2727
})

0 commit comments

Comments
 (0)