Add initial project structure including: - .gitignore and VSCode settings - Vite configuration and package.json with Vue 3 dependencies - Basic HTML entry point and README update - Core source files: App.vue, main.js, style.css - Markdown editor component with plugin system and related types - Sample HelloWorld component, router, and Pinia store - Assets and SVG icons This commit establishes the foundation for the Vue 3 application.
121 lines
2.9 KiB
Vue
121 lines
2.9 KiB
Vue
<template>
|
|
<!-- Textarea for markdown input -->
|
|
<textarea
|
|
v-model="markdown"
|
|
@keydown="handleKeydown"
|
|
placeholder="Enter markdown..."
|
|
rows="10"
|
|
style="width: 100%;"
|
|
></textarea>
|
|
|
|
<!-- Plugin host (no UI) -->
|
|
<PluginHost />
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, watch, onMounted } from 'vue'
|
|
import { plugins } from '../plugins/index'
|
|
import PluginHost from './PluginHost.vue'
|
|
import markdownIt from 'markdown-it'
|
|
import Prism from 'prismjs'
|
|
// 按需加载常用语言
|
|
import 'prismjs/components/prism-javascript'
|
|
import 'prismjs/components/prism-python'
|
|
import 'prismjs/components/prism-css'
|
|
|
|
/* ---------- 插件挂载点 ---------- */
|
|
const pluginContext = {}
|
|
onMounted(() => {
|
|
plugins.forEach(p => {
|
|
if (p.onSetup) p.onSetup(pluginContext)
|
|
})
|
|
})
|
|
|
|
/* ---------- markdown 解析 ---------- */
|
|
const md = markdownIt({
|
|
highlight: (code, lang) => {
|
|
if (lang && Prism.languages[lang]) {
|
|
return `<pre class="language-${lang}"><code>${Prism.highlight(
|
|
code,
|
|
Prism.languages[lang],
|
|
lang
|
|
)}</code></pre>`
|
|
}
|
|
return `<pre class="language-${lang}"><code>${md.utils.escapeHtml(
|
|
code
|
|
)}</code></pre>`
|
|
}
|
|
})
|
|
|
|
const markdown = ref('')
|
|
|
|
/* ---------- 解析后钩子 & emit ---------- */
|
|
watch(
|
|
markdown,
|
|
(newVal) => {
|
|
let html = md.render(newVal)
|
|
|
|
// onAfterParse 插件
|
|
const afterPayload = { markdown: newVal, html }
|
|
plugins.forEach(p => {
|
|
if (p.onAfterParse) {
|
|
const res = p.onAfterParse(afterPayload)
|
|
if (res && res.html) afterPayload.html = res.html
|
|
}
|
|
})
|
|
|
|
// onBeforeRender 插件
|
|
const beforePayload = { html: afterPayload.html }
|
|
plugins.forEach(p => {
|
|
if (p.onBeforeRender) {
|
|
const res = p.onBeforeRender(beforePayload)
|
|
if (res && res.html) beforePayload.html = res.html
|
|
}
|
|
})
|
|
|
|
emit('update:html', beforePayload.html)
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
|
|
/* ---------- 键盘快捷键 ---------- */
|
|
function insertAtCursor(text) {
|
|
const el = document.activeElement
|
|
if (el && el.selectionStart !== undefined) {
|
|
const start = el.selectionStart
|
|
const end = el.selectionEnd
|
|
const before = markdown.value.slice(0, start)
|
|
const after = markdown.value.slice(end)
|
|
markdown.value = `${before}${text}${after}`
|
|
// 将光标放在插入文本后
|
|
const pos = start + text.length
|
|
nextTick(() => {
|
|
el.setSelectionRange(pos, pos)
|
|
})
|
|
}
|
|
}
|
|
|
|
function handleKeydown(e) {
|
|
if (e.ctrlKey && !e.shiftKey) {
|
|
if (e.key === 'b' || e.key === 'B') {
|
|
e.preventDefault()
|
|
insertAtCursor('**粗体**')
|
|
} else if (e.key === 'i' || e.key === 'I') {
|
|
e.preventDefault()
|
|
insertAtCursor('_斜体_')
|
|
} else if (e.key === '`') {
|
|
e.preventDefault()
|
|
insertAtCursor('```\n代码块\n```')
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
textarea {
|
|
font-family: inherit;
|
|
font-size: 1rem;
|
|
padding: 0.5rem;
|
|
box-sizing: border-box;
|
|
}
|
|
</style> |