添加复制和下载按钮,统计字数功能
This commit is contained in:
@@ -12,6 +12,155 @@ import './MarkdownRenderer.css';
|
||||
import 'katex/dist/katex.min.css';
|
||||
import 'highlight.js/styles/github.css';
|
||||
|
||||
// 下载Markdown文件功能
|
||||
function downloadMarkdown(content, filename) {
|
||||
const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename || 'document.md';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// 复制到剪贴板功能
|
||||
async function copyToClipboard(content) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content);
|
||||
return true;
|
||||
} catch (err) {
|
||||
// 降级方案:使用传统的复制方法
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = content;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-999999px';
|
||||
textArea.style.top = '-999999px';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
return true;
|
||||
} catch (err) {
|
||||
document.body.removeChild(textArea);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 功能按钮组件
|
||||
function ActionButtons({ content, filename }) {
|
||||
const [copyStatus, setCopyStatus] = useState('');
|
||||
const [downloadStatus, setDownloadStatus] = useState('');
|
||||
|
||||
const handleDownload = () => {
|
||||
try {
|
||||
downloadMarkdown(content, filename);
|
||||
setDownloadStatus('success');
|
||||
setTimeout(() => setDownloadStatus(''), 2000);
|
||||
} catch (error) {
|
||||
setDownloadStatus('error');
|
||||
setTimeout(() => setDownloadStatus(''), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopy = async () => {
|
||||
const success = await copyToClipboard(content);
|
||||
setCopyStatus(success ? 'success' : 'error');
|
||||
setTimeout(() => setCopyStatus(''), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="action-buttons">
|
||||
<button
|
||||
className={`action-button download-button ${downloadStatus}`}
|
||||
onClick={handleDownload}
|
||||
title="下载Markdown文件"
|
||||
aria-label="下载Markdown文件"
|
||||
>
|
||||
📥
|
||||
</button>
|
||||
<button
|
||||
className={`action-button copy-button ${copyStatus}`}
|
||||
onClick={handleCopy}
|
||||
title="复制Markdown内容"
|
||||
aria-label="复制Markdown内容"
|
||||
>
|
||||
📋
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 字数统计工具函数
|
||||
function countWords(markdownText) {
|
||||
if (!markdownText || typeof markdownText !== 'string') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 移除Markdown格式符号的正则表达式
|
||||
let plainText = markdownText
|
||||
// 移除代码块
|
||||
.replace(/```[\s\S]*?```/g, '')
|
||||
// 移除内联代码
|
||||
.replace(/`[^`]*`/g, '')
|
||||
// 移除链接 [text](url)
|
||||
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
|
||||
// 移除图片 
|
||||
.replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
|
||||
// 移除标题标记
|
||||
.replace(/^#{1,6}\s+/gm, '')
|
||||
// 移除粗体和斜体标记
|
||||
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
||||
.replace(/\*([^*]+)\*/g, '$1')
|
||||
.replace(/__([^_]+)__/g, '$1')
|
||||
.replace(/_([^_]+)_/g, '$1')
|
||||
// 移除删除线
|
||||
.replace(/~~([^~]+)~~/g, '$1')
|
||||
// 移除引用标记
|
||||
.replace(/^>\s*/gm, '')
|
||||
// 移除列表标记
|
||||
.replace(/^[\s]*[-*+]\s+/gm, '')
|
||||
.replace(/^[\s]*\d+\.\s+/gm, '')
|
||||
// 移除水平分割线
|
||||
.replace(/^[-*_]{3,}$/gm, '')
|
||||
// 移除HTML标签
|
||||
.replace(/<[^>]*>/g, '')
|
||||
// 移除多余的空白字符
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
||||
// 统计中文字符和英文单词
|
||||
const chineseChars = (plainText.match(/[\u4e00-\u9fa5]/g) || []).length;
|
||||
const englishWords = plainText
|
||||
.replace(/[\u4e00-\u9fa5]/g, ' ')
|
||||
.split(/\s+/)
|
||||
.filter(word => word.length > 0 && /[a-zA-Z]/.test(word)).length;
|
||||
|
||||
return chineseChars + englishWords;
|
||||
}
|
||||
|
||||
// 字数统计显示组件
|
||||
function WordCount({ content }) {
|
||||
const wordCount = useMemo(() => countWords(content), [content]);
|
||||
|
||||
if (wordCount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="word-count-container">
|
||||
<div className="word-count-info">
|
||||
<span className="word-count-icon">📊</span>
|
||||
<span className="word-count-text">全文共 {wordCount.toLocaleString()} 字</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 自定义插件:禁用内联代码解析
|
||||
function remarkDisableInlineCode() {
|
||||
return (tree) => {
|
||||
@@ -250,11 +399,13 @@ export default function MarkdownRenderer() {
|
||||
}
|
||||
|
||||
const fileTitle = getFileTitle(currentFile.split('/').pop(), currentContent);
|
||||
const filename = currentFile.split('/').pop();
|
||||
|
||||
return (
|
||||
<div className={contentAreaClass}>
|
||||
<div className="content-header">
|
||||
<h1 className="content-title">{fileTitle}</h1>
|
||||
<ActionButtons content={currentContent} filename={filename} />
|
||||
</div>
|
||||
|
||||
<div className="content-body">
|
||||
@@ -273,6 +424,7 @@ export default function MarkdownRenderer() {
|
||||
>
|
||||
{currentContent}
|
||||
</ReactMarkdown>
|
||||
<WordCount content={currentContent} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user