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:
2026-01-12 12:23:44 +08:00
parent 2517aa27d5
commit 49f264b53b
20 changed files with 2261 additions and 1 deletions

View 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>

View 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>

View 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>