feat(project): scaffold Vue 3 + Vite markdown editor
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.
This commit is contained in:
43
src/components/HelloWorld.vue
Normal file
43
src/components/HelloWorld.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps({
|
||||
msg: String,
|
||||
})
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<div class="card">
|
||||
<button type="button" @click="count++">count is {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test HMR
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Check out
|
||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||
>create-vue</a
|
||||
>, the official Vue + Vite starter
|
||||
</p>
|
||||
<p>
|
||||
Learn more about IDE Support for Vue in the
|
||||
<a
|
||||
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||
target="_blank"
|
||||
>Vue Docs Scaling up Guide</a
|
||||
>.
|
||||
</p>
|
||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
121
src/components/MarkdownEditor.vue
Normal file
121
src/components/MarkdownEditor.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<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>
|
||||
23
src/components/PluginHost.vue
Normal file
23
src/components/PluginHost.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<!-- PluginHost does not render UI; it only runs plugin lifecycle hooks -->
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { plugins } from '../plugins/index'
|
||||
|
||||
/**
|
||||
* Context object that can be extended by plugins during onSetup.
|
||||
* Currently empty but can hold shared resources.
|
||||
*/
|
||||
const context = {}
|
||||
|
||||
onMounted(() => {
|
||||
// Invoke onSetup hook of each registered plugin
|
||||
plugins.forEach(p => {
|
||||
if (p.onSetup) {
|
||||
p.onSetup(context)
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user