添加复制和下载按钮,统计字数功能

This commit is contained in:
2025-10-12 19:32:52 +08:00
parent 34439f5cab
commit f8bd6388d7
77 changed files with 6374 additions and 599 deletions

View File

@@ -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')
// 移除图片 ![alt](url)
.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>