feat(editor): implement WYSIWYG Markdown editor using Milkdown Crepe
Replace the existing contenteditable-based markdown editor with a full-featured WYSIWYG editor using @milkdown/crepe. The new implementation provides: - True WYSIWYG editing experience with instant Markdown syntax rendering - Slash command menu support for quick formatting - Code block highlighting and image paste support - Built-in export to markdown file functionality Changes include new MilkdownEditor component, updated App.vue integration, theme styling imports, and optimized Vite configuration for the new dependencies.
This commit is contained in:
Generated
+2366
File diff suppressed because it is too large
Load Diff
+9
-4
@@ -9,12 +9,17 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@milkdown/core": "^7.18.0",
|
||||
"@milkdown/crepe": "^7.18.0",
|
||||
"@milkdown/kit": "^7.18.0",
|
||||
"@milkdown/theme-nord": "^7.18.0",
|
||||
"@milkdown/vue": "^7.18.0",
|
||||
"axios": "^1.13.2",
|
||||
"pinia": "^2.3.1",
|
||||
"vue": "^3.5.24",
|
||||
"vue-router": "^4.6.4",
|
||||
"markdown-it": "^13.0.0",
|
||||
"prismjs": "^1.29.0"
|
||||
"pinia": "^2.3.1",
|
||||
"prismjs": "^1.29.0",
|
||||
"vue": "^3.5.24",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# Milkdown 全屏 WYSIWYG Markdown 编辑器实施方案
|
||||
|
||||
## 项目概述
|
||||
|
||||
基于 `@milkdown/crepe` 实现全屏覆盖的所见即所得 Markdown 编辑器,替换现有的 contenteditable 编辑器。
|
||||
|
||||
## 技术选型
|
||||
|
||||
- **核心编辑器**: `@milkdown/crepe` - 功能完整的 WYSIWYG Markdown 编辑器
|
||||
- **Vue 集成**: `@milkdown/vue` - Vue 3 组件支持
|
||||
- **主题**: `frame`(简洁框架主题)
|
||||
|
||||
## 系统架构
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
A[App.vue] --> B[MilkdownProvider]
|
||||
B --> C[Milkdown Editor - Crepe]
|
||||
|
||||
subgraph "Crepe 核心功能"
|
||||
D[WYSWIYG 编辑体验]
|
||||
E[Markdown 语法即时渲染]
|
||||
F[斜杠命令菜单 Slash Commands]
|
||||
G[代码块高亮]
|
||||
H[图片粘贴支持]
|
||||
end
|
||||
```
|
||||
|
||||
## 实施步骤
|
||||
|
||||
### Step 1: 安装依赖包
|
||||
|
||||
```bash
|
||||
npm install @milkdown/crepe @milkdown/vue
|
||||
```
|
||||
|
||||
### Step 2: 创建 Milkdown 编辑器组件
|
||||
|
||||
**文件**: `src/components/MilkdownEditor.vue`
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<MilkdownProvider>
|
||||
<Milkdown />
|
||||
</MilkdownProvider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Milkdown, MilkdownProvider, useEditor } from '@milkdown/vue'
|
||||
import { Crepe } from '@milkdown/crepe'
|
||||
|
||||
const { get } = useEditor((root) => new Crepe({ root }))
|
||||
</script>
|
||||
```
|
||||
|
||||
### Step 3: 更新 App.vue
|
||||
|
||||
- 移除旧的 `MarkdownEditor` 组件引用
|
||||
- 使用新的 `MilkdownEditor` 组件
|
||||
- 保持全屏布局(100vh × 100vw)
|
||||
|
||||
### Step 4: 添加样式配置
|
||||
|
||||
在 `main.js` 中导入 Crepe 主题:
|
||||
```js
|
||||
import '@milkdown/crepe/theme/common/style.css'
|
||||
import '@milkdown/crepe/theme/frame.css'
|
||||
```
|
||||
|
||||
## 全屏覆盖样式要点
|
||||
|
||||
- 编辑器容器: `width: 100vw; height: 100vh`
|
||||
- 移除默认 padding/margin
|
||||
- 纯编辑器模式,无预览面板
|
||||
+9
-34
@@ -1,41 +1,16 @@
|
||||
<script setup>
|
||||
import MarkdownEditor from './components/MarkdownEditor.vue'
|
||||
import MilkdownEditor from './components/MilkdownEditor.vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const html = ref('')
|
||||
const markdown = ref('')
|
||||
const emit = defineEmits(['update:markdown'])
|
||||
|
||||
function onChange(markdownValue) {
|
||||
markdown.value = markdownValue
|
||||
emit('update:markdown', markdownValue)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="typora-container">
|
||||
<MarkdownEditor @update:html="html = $event" />
|
||||
<div class="preview" v-html="html"></div>
|
||||
</div>
|
||||
<MilkdownEditor @update:markdown="onChange" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.typora-container {
|
||||
display: flex;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview {
|
||||
flex: 1;
|
||||
padding: 2rem;
|
||||
overflow-y: auto;
|
||||
background: #f9f9f9;
|
||||
border-left: 1px solid #ddd;
|
||||
}
|
||||
|
||||
/* 图片点击放大 */
|
||||
.preview :deep(img) {
|
||||
max-width: 100%;
|
||||
cursor: zoom-in;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.preview :deep(img:hover) {
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div class="editor-container">
|
||||
<button class="export-btn" @click="exportMarkdown">导出文件</button>
|
||||
<div ref="root" class="milkdown-editor"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { Crepe } from '@milkdown/crepe'
|
||||
|
||||
const root = ref(null)
|
||||
let crepe = null
|
||||
|
||||
onMounted(async () => {
|
||||
if (!root.value) return
|
||||
|
||||
crepe = new Crepe({
|
||||
root: root.value,
|
||||
defaultValue: '# Welcome to Milkdown\n\nStart writing your markdown content here...',
|
||||
})
|
||||
|
||||
await crepe.create()
|
||||
})
|
||||
|
||||
const exportMarkdown = async () => {
|
||||
if (!crepe) return
|
||||
const markdown = await crepe.getMarkdown()
|
||||
const blob = new Blob([markdown], { type: 'text/markdown' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `document-${Date.now()}.md`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.editor-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.export-btn {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 8px 16px;
|
||||
background-color: #4a90d9;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.export-btn:hover {
|
||||
background-color: #3a7bc8;
|
||||
}
|
||||
|
||||
.milkdown-editor {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: #ffffff;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 当内容不超过视口时隐藏滚动条 */
|
||||
.milkdown-editor::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.milkdown-editor::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.milkdown-editor::-webkit-scrollbar-thumb {
|
||||
background-color: #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 编辑器内容容器样式 */
|
||||
.milkdown-editor :deep(.milkdown) {
|
||||
max-width: 900px;
|
||||
margin: 0 auto !important;
|
||||
padding: 20px 40px !important;
|
||||
min-height: calc(100vh - 40px);
|
||||
}
|
||||
|
||||
/* 全局覆盖所有元素的边距 */
|
||||
.milkdown-editor :deep(*) {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -2,4 +2,7 @@ import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import './style.css'
|
||||
|
||||
import '@milkdown/crepe/theme/common/style.css'
|
||||
import '@milkdown/crepe/theme/frame.css'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
|
||||
+12
-103
@@ -3,9 +3,9 @@
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
color-scheme: light;
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
@@ -19,15 +19,14 @@ a {
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
color: #747bff;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@@ -42,7 +41,7 @@ button {
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
background-color: #f9f9f9;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
@@ -59,83 +58,11 @@ button:focus-visible {
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.editor-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.editor-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
.editor, .preview {
|
||||
flex: 1;
|
||||
padding: 1rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Editor textarea */
|
||||
.editor textarea,
|
||||
.editor input,
|
||||
.editor .input {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
line-height: 1.6;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
/* Selection */
|
||||
.editor ::selection {
|
||||
background: rgba(200, 200, 200, 0.3);
|
||||
}
|
||||
|
||||
/* Preview typography */
|
||||
.preview p {
|
||||
line-height: 1.6;
|
||||
}
|
||||
.preview h1 { font-size: 2em; }
|
||||
.preview h2 { font-size: 1.75em; }
|
||||
.preview h3 { font-size: 1.5em; }
|
||||
.preview h4 { font-size: 1.25em; }
|
||||
.preview h5 { font-size: 1em; }
|
||||
.preview h6 { font-size: 0.875em; }
|
||||
.preview blockquote {
|
||||
border-left: 4px solid #ddd;
|
||||
padding-left: 1rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.preview table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
.preview th,
|
||||
.preview td {
|
||||
border: 1px solid #ccc;
|
||||
padding: 0.4rem;
|
||||
}
|
||||
|
||||
/* Code block */
|
||||
.preview pre,
|
||||
.preview code {
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
padding: 0.8rem;
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
/* Global */
|
||||
@@ -143,25 +70,7 @@ html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* CSS variables (to be defined elsewhere) */
|
||||
:root {
|
||||
--bg-color: var(--bg-color);
|
||||
--text-color: var(--text-color);
|
||||
}
|
||||
|
||||
/* Light mode overrides (original) */
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
+11
-1
@@ -1,7 +1,17 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
host: true,
|
||||
port: 5173
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: [
|
||||
'@milkdown/crepe',
|
||||
'@milkdown/vue',
|
||||
'@milkdown/kit'
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user