duqing2026's picture
Enhance: Add Toolbar, Notion Theme, Pangu spacing, and Interview Guide
4167172
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 `<span class="link-text">`;
};
md.renderer.rules.link_close = function (tokens, idx, options, env, self) {
const n = footnotes.length;
return `<sup>[${n}]</sup></span>`;
};
const htmlContent = computed(() => {
footnotes = [];
let rendered = md.render(markdown.value);
if (footnotes.length > 0) {
rendered += `<div class="footnotes-sep"></div><div class="footnotes-list">`;
rendered += `<h3>引用链接</h3><ol>`;
footnotes.forEach((url) => {
rendered += `<li>${url}</li>`;
});
rendered += `</ol></div>`;
}
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');