const { createApp, ref, computed, onMounted, watch } = Vue; createApp({ setup() { const markdown = ref(`# 欢迎使用微信 Markdown 编辑器 这是一个专为微信公众号设计的 **Markdown** 编辑器。 ## 新增功能 ✨ - **一键格式化**:自动在中英文之间添加空格(盘古之白)。 - **多主题支持**:新增 **Notion** 风格。 - **图片美化**:支持圆角和阴影。 - **字数统计**:实时显示字数和阅读时间。 ## 代码示例 \`\`\`python def hello_world(): print("Hello, WeChat!") \`\`\` ## 引用链接 微信公众号不支持外部链接,本编辑器会自动将链接转换为脚注。 例如:[GitHub](https://github.com) 和 [Google](https://google.com)。 > 极致的排版体验。 `); const currentTheme = ref('default'); const imgStyle = ref('none'); // none, rounded, shadow, both const copied = ref(false); const editorRef = ref(null); const previewRef = ref(null); let isScrolling = false; // Statistics const wordCount = computed(() => { // Simple logic: remove markdown symbols and count const text = markdown.value.replace(/[#*>\`\-]/g, '').trim(); return text.length; }); const readTime = computed(() => { return Math.ceil(wordCount.value / 400); // 400 chars per minute }); // Pangu (Auto Space) Logic const formatContent = () => { let text = markdown.value; // Add space between CJK and English/Number text = text.replace(/([\u4e00-\u9fa5])([a-zA-Z0-9])/g, '$1 $2'); text = text.replace(/([a-zA-Z0-9])([\u4e00-\u9fa5])/g, '$1 $2'); markdown.value = text; }; // Markdown-it setup const md = window.markdownit({ html: true, breaks: true, linkify: true, typographer: true, highlight: function (str, lang) { if (lang && hljs.getLanguage(lang)) { try { return hljs.highlight(str, { language: lang, ignoreIllegals: true }).value; } catch (__) {} } return ''; } }); // Footnotes logic const defaultRender = md.renderer.rules.link_open || function(tokens, idx, options, env, self) { return self.renderToken(tokens, idx, options); }; let footnotes = []; md.renderer.rules.link_open = function (tokens, idx, options, env, self) { const href = tokens[idx].attrGet('href'); if (!href || href.startsWith('#')) { return defaultRender(tokens, idx, options, env, self); } footnotes.push(href); return ``; }; md.renderer.rules.link_close = function (tokens, idx, options, env, self) { const n = footnotes.length; return `[${n}]`; }; const htmlContent = computed(() => { footnotes = []; let rendered = md.render(markdown.value); if (footnotes.length > 0) { rendered += `
`; rendered += `

引用链接

    `; footnotes.forEach((url) => { rendered += `
  1. ${url}
  2. `; }); rendered += `
`; } return rendered; }); const updateTheme = () => { document.body.setAttribute('data-theme', currentTheme.value); }; const updateImgStyle = () => { document.body.setAttribute('data-img-style', imgStyle.value); }; const copyToWeChat = () => { const range = document.createRange(); range.selectNode(document.getElementById('preview-content')); window.getSelection().removeAllRanges(); window.getSelection().addRange(range); document.execCommand('copy'); window.getSelection().removeAllRanges(); copied.value = true; setTimeout(() => { copied.value = false; }, 2000); }; // Insert Markdown syntax at cursor const insertSyntax = (type) => { const textarea = editorRef.value; const start = textarea.selectionStart; const end = textarea.selectionEnd; const text = markdown.value; const selected = text.substring(start, end); let before = ''; let after = ''; switch(type) { case 'bold': before = '**'; after = '**'; break; case 'italic': before = '*'; after = '*'; break; case 'code': before = '`'; after = '`'; break; case 'quote': before = '> '; after = ''; break; case 'link': before = '['; after = '](url)'; break; case 'image': before = '!['; after = '](https://)'; break; case 'h2': before = '## '; after = ''; break; case 'h3': before = '### '; after = ''; break; case 'hr': before = '\n---\n'; after = ''; break; } const newText = text.substring(0, start) + before + selected + after + text.substring(end); markdown.value = newText; // Restore focus (approximate) setTimeout(() => { textarea.focus(); textarea.setSelectionRange(start + before.length, end + before.length); }, 0); }; const handleScroll = (source) => { if (isScrolling) return; isScrolling = true; const editor = editorRef.value; const preview = previewRef.value; if (source === 'editor') { const percent = editor.scrollTop / (editor.scrollHeight - editor.clientHeight); preview.scrollTop = percent * (preview.scrollHeight - preview.clientHeight); } else { const percent = preview.scrollTop / (preview.scrollHeight - preview.clientHeight); editor.scrollTop = percent * (editor.scrollHeight - editor.clientHeight); } setTimeout(() => { isScrolling = false; }, 50); }; onMounted(() => { updateTheme(); updateImgStyle(); }); watch(currentTheme, updateTheme); watch(imgStyle, updateImgStyle); return { markdown, htmlContent, currentTheme, imgStyle, copied, wordCount, readTime, editorRef, previewRef, formatContent, copyToWeChat, insertSyntax, handleScroll }; } }).mount('#app');