|
@@ -0,0 +1,124 @@
|
|
|
+<template>
|
|
|
+ <div class="markdown-container">
|
|
|
+ <div class="toolbar">
|
|
|
+ <button @click="insertText('**', '**')">B</button>
|
|
|
+ <button @click="insertText('*', '*')">I</button>
|
|
|
+ <button @click="insertText('# ', '')">H1</button>
|
|
|
+ <button @click="insertText('## ', '')">H2</button>
|
|
|
+ <button @click="insertText('[', '](url)')">Link</button>
|
|
|
+ <button @click="insertText('')">Image</button>
|
|
|
+ </div>
|
|
|
+ <div class="editor-wrapper">
|
|
|
+ <textarea
|
|
|
+ ref="textarea"
|
|
|
+ v-model="markdownText"
|
|
|
+ class="editor"
|
|
|
+ @input="updateMarkdown"
|
|
|
+ ></textarea>
|
|
|
+ <div class="preview" v-html="compiledMarkdown"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+defineOptions({
|
|
|
+ name: 'CzrMarkdown',
|
|
|
+})
|
|
|
+import { ref, computed, onMounted } from 'vue'
|
|
|
+import { marked } from 'marked'
|
|
|
+import DOMPurify from 'dompurify'
|
|
|
+
|
|
|
+const emit = defineEmits(['update:modelValue'])
|
|
|
+const props = defineProps({
|
|
|
+ modelValue: {
|
|
|
+ type: String,
|
|
|
+ default: '',
|
|
|
+ },
|
|
|
+})
|
|
|
+const textarea = ref(null)
|
|
|
+const markdownText = ref(props.modelValue)
|
|
|
+
|
|
|
+const compiledMarkdown = computed(() => {
|
|
|
+ return DOMPurify.sanitize(marked(markdownText.value))
|
|
|
+})
|
|
|
+
|
|
|
+function updateMarkdown() {
|
|
|
+ emit('update:modelValue', markdownText.value)
|
|
|
+}
|
|
|
+
|
|
|
+function insertText(before, after) {
|
|
|
+ const textareaEl = textarea.value
|
|
|
+ const start = textareaEl.selectionStart
|
|
|
+ const end = textareaEl.selectionEnd
|
|
|
+ const selectedText = markdownText.value.substring(start, end)
|
|
|
+
|
|
|
+ markdownText.value =
|
|
|
+ markdownText.value.substring(0, start) +
|
|
|
+ before +
|
|
|
+ selectedText +
|
|
|
+ after +
|
|
|
+ markdownText.value.substring(end)
|
|
|
+
|
|
|
+ // 移动光标位置
|
|
|
+ setTimeout(() => {
|
|
|
+ textareaEl.selectionStart = start + before.length
|
|
|
+ textareaEl.selectionEnd = end + before.length
|
|
|
+ textareaEl.focus()
|
|
|
+ }, 0)
|
|
|
+
|
|
|
+ emit('update:modelValue', markdownText.value)
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ marked.setOptions({
|
|
|
+ breaks: true,
|
|
|
+ gfm: true,
|
|
|
+ })
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style>
|
|
|
+.markdown-container {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.toolbar {
|
|
|
+ padding: 8px;
|
|
|
+ background: #f5f5f5;
|
|
|
+ border-bottom: 1px solid #ddd;
|
|
|
+}
|
|
|
+
|
|
|
+.toolbar button {
|
|
|
+ margin-right: 8px;
|
|
|
+ padding: 4px 8px;
|
|
|
+ background: white;
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.editor-wrapper {
|
|
|
+ display: flex;
|
|
|
+ flex: 1;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.editor {
|
|
|
+ flex: 1;
|
|
|
+ padding: 10px;
|
|
|
+ border: none;
|
|
|
+ border-right: 1px solid #ddd;
|
|
|
+ resize: none;
|
|
|
+ font-family: monospace;
|
|
|
+ outline: none;
|
|
|
+}
|
|
|
+
|
|
|
+.preview {
|
|
|
+ flex: 1;
|
|
|
+ padding: 10px;
|
|
|
+ overflow-y: auto;
|
|
|
+ background: white;
|
|
|
+}
|
|
|
+</style>
|