chore: sync local changes (2026-03-12)

This commit is contained in:
2026-03-12 18:58:47 +08:00
parent cf2203e3eb
commit 6c60d9c8a0
478 changed files with 5690 additions and 53453 deletions

View File

@@ -1,96 +1,96 @@
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('[MengyaNote] Error Boundary 捕获错误:', error, errorInfo);
this.setState({
error,
errorInfo
});
}
render() {
if (this.state.hasError) {
return (
<div style={{
padding: '40px',
textAlign: 'center',
maxWidth: '600px',
margin: '100px auto',
backgroundColor: '#fff3cd',
border: '1px solid #ffc107',
borderRadius: '8px'
}}>
<h1 style={{ color: '#856404', marginBottom: '20px' }}> 应用出现错误</h1>
<p style={{ color: '#856404', marginBottom: '15px' }}>
抱歉应用遇到了一个问题这可能是由于浏览器权限设置导致的
</p>
<div style={{
backgroundColor: '#f8f9fa',
padding: '15px',
borderRadius: '4px',
marginBottom: '20px',
textAlign: 'left',
fontFamily: 'monospace',
fontSize: '12px',
overflow: 'auto'
}}>
<strong>错误信息:</strong>
<pre style={{ margin: '10px 0', whiteSpace: 'pre-wrap' }}>
{this.state.error && this.state.error.toString()}
</pre>
</div>
<div style={{ marginTop: '20px' }}>
<button
onClick={() => window.location.reload()}
style={{
padding: '10px 20px',
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}}
>
刷新页面重试
</button>
</div>
<div style={{
marginTop: '30px',
padding: '15px',
backgroundColor: '#e7f3ff',
border: '1px solid #b3d9ff',
borderRadius: '4px',
textAlign: 'left',
fontSize: '13px'
}}>
<strong>💡 可能的解决方案:</strong>
<ul style={{ marginTop: '10px', paddingLeft: '20px' }}>
<li>尝试在浏览器设置中允许此网站访问存储</li>
<li>清除浏览器缓存和Cookie后重试</li>
<li>尝试使用无痕/隐私模式访问</li>
<li>更换其他浏览器推荐 ChromeEdgeFirefox</li>
</ul>
</div>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('[MengyaNote] Error Boundary 捕获错误:', error, errorInfo);
this.setState({
error,
errorInfo
});
}
render() {
if (this.state.hasError) {
return (
<div style={{
padding: '40px',
textAlign: 'center',
maxWidth: '600px',
margin: '100px auto',
backgroundColor: '#fff3cd',
border: '1px solid #ffc107',
borderRadius: '8px'
}}>
<h1 style={{ color: '#856404', marginBottom: '20px' }}> 应用出现错误</h1>
<p style={{ color: '#856404', marginBottom: '15px' }}>
抱歉应用遇到了一个问题这可能是由于浏览器权限设置导致的
</p>
<div style={{
backgroundColor: '#f8f9fa',
padding: '15px',
borderRadius: '4px',
marginBottom: '20px',
textAlign: 'left',
fontFamily: 'monospace',
fontSize: '12px',
overflow: 'auto'
}}>
<strong>错误信息:</strong>
<pre style={{ margin: '10px 0', whiteSpace: 'pre-wrap' }}>
{this.state.error && this.state.error.toString()}
</pre>
</div>
<div style={{ marginTop: '20px' }}>
<button
onClick={() => window.location.reload()}
style={{
padding: '10px 20px',
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}}
>
刷新页面重试
</button>
</div>
<div style={{
marginTop: '30px',
padding: '15px',
backgroundColor: '#e7f3ff',
border: '1px solid #b3d9ff',
borderRadius: '4px',
textAlign: 'left',
fontSize: '13px'
}}>
<strong>💡 可能的解决方案:</strong>
<ul style={{ marginTop: '10px', paddingLeft: '20px' }}>
<li>尝试在浏览器设置中允许此网站访问存储</li>
<li>清除浏览器缓存和Cookie后重试</li>
<li>尝试使用无痕/隐私模式访问</li>
<li>更换其他浏览器推荐 ChromeEdgeFirefox</li>
</ul>
</div>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@@ -1,321 +1,528 @@
/* ========================================
Markdown 渲染器样式文件
用途:定义 Markdown 内容渲染容器的布局和样式
======================================== */
/* ========================================
主容器样式
======================================== */
.markdown-renderer {
flex: 1;
height: 100vh;
overflow-y: auto;
overflow-x: hidden;
background: #ffffff;
/* 隐藏滚动条但保留滚动功能 */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
.markdown-renderer::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
/* ========================================
内容容器
======================================== */
.markdown-container {
width: 100%;
margin: 0;
padding: 45px;
min-height: 100vh;
}
/* ========================================
GitHub Markdown Body 样式增强
======================================== */
.markdown-body {
box-sizing: border-box;
min-width: 200px;
font-family: 'Maple Mono CN', ui-monospace, -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji" !important;
font-size: 20.8px; /* 16px * 1.3 = 20.8px */
line-height: 1.6;
word-wrap: break-word;
background-color: transparent;
}
/* 覆盖 github-markdown-css 的字体设置 */
.markdown-body,
.markdown-body p,
.markdown-body li,
.markdown-body td,
.markdown-body th,
.markdown-body div,
.markdown-body span {
font-family: 'Maple Mono CN', ui-monospace, -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif !important;
}
/* 代码字体保持等宽 */
.markdown-body code,
.markdown-body pre,
.markdown-body tt {
font-family: 'Maple Mono CN', ui-monospace, 'SFMono-Regular', 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace !important;
}
/* 图片可点击放大效果 */
.markdown-body img {
transition: transform 0.3s ease;
max-width: 100%;
}
.markdown-body img.zoomed {
transform: scale(1.5);
cursor: zoom-out;
position: relative;
z-index: 10;
}
/* 表格优化 */
.markdown-body table {
display: block;
width: max-content;
max-width: 100%;
overflow: auto;
}
/* ========================================
加载状态样式
======================================== */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 400px;
color: #57606a;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f0f0f0;
border-top: 4px solid #0969da;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-state p {
margin: 0;
font-size: 14px;
}
/* ========================================
错误状态样式
======================================== */
.error-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 400px;
color: #cf222e;
text-align: center;
padding: 24px;
}
.error-icon {
font-size: 48px;
margin-bottom: 16px;
}
.error-state h2 {
margin: 0 0 12px 0;
font-size: 24px;
font-weight: 600;
color: #24292f;
}
.error-state p {
margin: 0;
font-size: 14px;
color: #57606a;
max-width: 500px;
}
/* ========================================
空状态样式
======================================== */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 400px;
color: #57606a;
text-align: center;
padding: 24px;
}
.empty-icon {
font-size: 64px;
margin-bottom: 20px;
opacity: 0.6;
}
.empty-state h2 {
margin: 0 0 12px 0;
font-size: 24px;
font-weight: 600;
color: #24292f;
}
.empty-state p {
margin: 0;
font-size: 16px;
color: #57606a;
}
/* ========================================
响应式设计
======================================== */
@media (max-width: 768px) {
.markdown-container {
padding: 20px 16px;
}
.markdown-body {
font-size: 14px;
}
/* 移动端图片不支持放大 */
.markdown-body img {
cursor: default;
}
.markdown-body img.zoomed {
transform: none;
}
}
@media (max-width: 480px) {
.markdown-container {
padding: 16px 12px;
}
}
/* ========================================
数学公式样式优化
======================================== */
.markdown-body .katex {
font-size: 1.1em;
}
.markdown-body .katex-display {
margin: 1em 0;
overflow-x: auto;
overflow-y: hidden;
}
/* ========================================
代码块滚动条样式
======================================== */
.markdown-body pre {
overflow-x: auto;
}
.markdown-body pre::-webkit-scrollbar {
height: 8px;
}
.markdown-body pre::-webkit-scrollbar-track {
background: #f6f8fa;
border-radius: 4px;
}
.markdown-body pre::-webkit-scrollbar-thumb {
background: #d0d7de;
border-radius: 4px;
}
.markdown-body pre::-webkit-scrollbar-thumb:hover {
background: #afb8c1;
}
/* ========================================
文件元数据样式
======================================== */
.file-metadata {
margin-top: 60px;
padding-top: 32px;
}
.metadata-divider {
width: 100%;
height: 1px;
background: linear-gradient(to right, transparent, #d0d7de 20%, #d0d7de 80%, transparent);
margin-bottom: 24px;
}
.metadata-content {
display: flex;
flex-wrap: wrap;
gap: 16px 32px;
padding: 20px 24px;
background: #f6f8fa;
border-radius: 6px;
border: 1px solid #d0d7de;
}
.metadata-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
line-height: 1.5;
}
.metadata-label {
color: #57606a;
font-weight: 500;
}
.metadata-value {
color: #24292f;
font-weight: 600;
}
/* 响应式调整 */
@media (max-width: 768px) {
.file-metadata {
margin-top: 40px;
padding-top: 24px;
}
.metadata-content {
flex-direction: column;
gap: 12px;
padding: 16px;
}
.metadata-item {
font-size: 13px;
}
}
/* ========================================
Markdown 渲染器样式文件
用途:定义 Markdown 内容渲染容器的布局和样式
======================================== */
/* ========================================
主容器样式
======================================== */
.markdown-renderer {
flex: 1;
height: 100vh;
overflow-y: auto;
overflow-x: hidden;
background: #ffffff;
/* 隐藏滚动条但保留滚动功能 */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
.markdown-renderer::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
/* ========================================
内容容器
======================================== */
.markdown-container {
width: 100%;
margin: 0;
padding: 45px;
min-height: 100vh;
}
/* ========================================
文件头部样式
======================================== */
.file-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 32px;
padding-bottom: 16px;
border-bottom: 1px solid #d0d7de;
}
.file-title {
margin: 0;
font-size: 28px;
font-weight: 600;
color: #24292f;
font-family: 'LXGWWenKaiMono', ui-monospace, var(--font-family-base);
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-right: 16px;
}
.file-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
.action-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
min-width: 32px;
height: 32px;
padding: 0 10px;
background: #f6f8fa;
color: #57606a;
border: 1px solid #d0d7de;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 1px 3px rgba(31, 35, 40, 0.04);
white-space: nowrap;
font-family: 'LXGWWenKaiMono', ui-monospace, var(--font-family-base);
}
.action-button:hover {
background: #ffffff;
border-color: #d0d7de;
color: #24292f;
box-shadow: 0 2px 6px rgba(31, 35, 40, 0.08);
transform: translateY(-1px);
}
.action-button:active {
transform: translateY(0);
box-shadow: 0 1px 3px rgba(31, 35, 40, 0.06);
}
.copy-button.success {
background: #2da44e;
border-color: #2da44e;
color: #ffffff;
}
/* ========================================
GitHub Markdown Body 样式增强
======================================== */
.markdown-body {
box-sizing: border-box;
min-width: 200px;
font-family: 'LXGWWenKaiMono', ui-monospace, -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji" !important;
font-size: 20.8px; /* 16px * 1.3 = 20.8px */
line-height: 1.6;
word-wrap: break-word;
background-color: transparent;
}
/* 覆盖 github-markdown-css 的字体设置 */
.markdown-body,
.markdown-body p,
.markdown-body li,
.markdown-body td,
.markdown-body th,
.markdown-body div,
.markdown-body span {
font-family: 'LXGWWenKaiMono', ui-monospace, -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif !important;
}
/* 代码字体保持等宽 */
.markdown-body code,
.markdown-body pre,
.markdown-body tt {
font-family: 'LXGWWenKaiMono', ui-monospace, 'SFMono-Regular', 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace !important;
}
/* ========================================
DeepSeek 风格代码块样式
======================================== */
.code-block-wrapper {
margin: 16px 0;
border: 1px solid #d0d7de;
border-radius: 6px;
overflow: hidden;
background: #f6f8fa;
}
.code-block-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: #f6f8fa;
border-bottom: 1px solid #d0d7de;
}
.code-language {
font-size: 12px;
font-weight: 600;
color: #57606a;
text-transform: lowercase;
font-family: 'LXGWWenKaiMono', ui-monospace, monospace;
}
.code-actions {
display: flex;
gap: 6px;
}
.code-action-btn {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
font-size: 12px;
font-weight: 500;
color: #57606a;
background: transparent;
border: 1px solid transparent;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
font-family: 'LXGWWenKaiMono', ui-monospace, var(--font-family-base);
white-space: nowrap;
}
.code-action-btn:hover {
background: #ffffff;
border-color: #d0d7de;
color: #24292f;
}
.code-action-btn:active {
background: #f6f8fa;
transform: scale(0.98);
}
/* 代码块内的 pre 标签样式覆盖 */
.code-block-wrapper pre {
margin: 0 !important;
padding: 16px !important;
background: #ffffff !important;
border: none !important;
border-radius: 0 !important;
overflow-x: auto;
}
.code-block-wrapper pre code {
background: transparent !important;
padding: 0 !important;
font-size: 14px !important;
line-height: 1.6 !important;
}
/* ========================================
原有图片和表格样式
======================================== */
.markdown-body img {
transition: transform 0.3s ease;
max-width: 100%;
}
.markdown-body img.zoomed {
transform: scale(1.5);
cursor: zoom-out;
position: relative;
z-index: 10;
}
/* 表格优化 */
.markdown-body table {
display: block;
width: max-content;
max-width: 100%;
overflow: auto;
}
/* ========================================
加载状态样式
======================================== */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 400px;
color: #57606a;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f0f0f0;
border-top: 4px solid #0969da;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-state p {
margin: 0;
font-size: 14px;
}
/* ========================================
错误状态样式
======================================== */
.error-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 400px;
color: #cf222e;
text-align: center;
padding: 24px;
}
.error-icon {
font-size: 48px;
margin-bottom: 16px;
}
.error-state h2 {
margin: 0 0 12px 0;
font-size: 24px;
font-weight: 600;
color: #24292f;
}
.error-state p {
margin: 0;
font-size: 14px;
color: #57606a;
max-width: 500px;
}
/* ========================================
空状态样式
======================================== */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 400px;
color: #57606a;
text-align: center;
padding: 24px;
}
.empty-icon {
font-size: 64px;
margin-bottom: 20px;
opacity: 0.6;
}
.empty-state h2 {
margin: 0 0 12px 0;
font-size: 24px;
font-weight: 600;
color: #24292f;
}
.empty-state p {
margin: 0;
font-size: 16px;
color: #57606a;
}
/* ========================================
响应式设计
======================================== */
@media (max-width: 768px) {
.markdown-container {
padding: 20px 16px;
}
.markdown-body {
font-size: 14px;
}
.file-header {
margin-bottom: 24px;
padding-bottom: 12px;
}
.file-title {
font-size: 20px;
}
.action-button {
min-width: 28px;
height: 28px;
padding: 0 8px;
font-size: 11px;
}
/* 移动端代码块样式调整 */
.code-block-header {
padding: 6px 10px;
}
.code-language {
font-size: 11px;
}
.code-action-btn {
padding: 3px 6px;
font-size: 11px;
}
.code-block-wrapper pre {
padding: 12px !important;
}
.code-block-wrapper pre code {
font-size: 13px !important;
}
/* 移动端图片不支持放大 */
.markdown-body img {
cursor: default;
}
.markdown-body img.zoomed {
transform: none;
}
}
@media (max-width: 480px) {
.markdown-container {
padding: 16px 12px;
}
.file-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.file-title {
padding-right: 0;
white-space: normal;
word-break: break-word;
}
.file-actions {
align-self: flex-end;
}
}
/* ========================================
数学公式样式优化
======================================== */
.markdown-body .katex {
font-size: 1.1em;
}
.markdown-body .katex-display {
margin: 1em 0;
overflow-x: auto;
overflow-y: hidden;
}
/* ========================================
代码块滚动条样式
======================================== */
.markdown-body pre {
overflow-x: auto;
}
.markdown-body pre::-webkit-scrollbar {
height: 8px;
}
.markdown-body pre::-webkit-scrollbar-track {
background: #f6f8fa;
border-radius: 4px;
}
.markdown-body pre::-webkit-scrollbar-thumb {
background: #d0d7de;
border-radius: 4px;
}
.markdown-body pre::-webkit-scrollbar-thumb:hover {
background: #afb8c1;
}
/* ========================================
文件元数据样式
======================================== */
.file-metadata {
margin-top: 60px;
padding-top: 32px;
}
.metadata-divider {
width: 100%;
height: 1px;
background: linear-gradient(to right, transparent, #d0d7de 20%, #d0d7de 80%, transparent);
margin-bottom: 24px;
}
.metadata-content {
display: flex;
flex-wrap: wrap;
gap: 16px 32px;
padding: 20px 24px;
background: #f6f8fa;
border-radius: 6px;
border: 1px solid #d0d7de;
}
.metadata-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
line-height: 1.5;
}
.metadata-label {
color: #57606a;
font-weight: 500;
}
.metadata-value {
color: #24292f;
font-weight: 600;
}
/* 响应式调整 */
@media (max-width: 768px) {
.file-metadata {
margin-top: 40px;
padding-top: 24px;
}
.metadata-content {
flex-direction: column;
gap: 12px;
padding: 16px;
}
.metadata-item {
font-size: 13px;
}
}

View File

@@ -1,157 +1,312 @@
import React, { useEffect, useRef } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import remarkBreaks from 'remark-breaks';
import rehypeKatex from 'rehype-katex';
import rehypeHighlight from 'rehype-highlight';
import rehypeRaw from 'rehype-raw';
import { useApp } from '../context/AppContext';
import './MarkdownRenderer.css';
import 'github-markdown-css/github-markdown-light.css';
import 'katex/dist/katex.min.css';
import 'highlight.js/styles/github.css';
function MarkdownRenderer() {
const { currentContent, currentFile, currentMetadata, isLoading, error } = useApp();
const contentRef = useRef(null);
// 格式化文件大小
const formatFileSize = (bytes) => {
if (!bytes) return '0 B';
const kb = bytes / 1024;
if (kb < 1024) return `${kb.toFixed(2)} KB`;
const mb = kb / 1024;
if (mb < 1024) return `${mb.toFixed(2)} MB`;
const gb = mb / 1024;
return `${gb.toFixed(2)} GB`;
};
// 当内容变化时,滚动到顶部
useEffect(() => {
if (contentRef.current) {
contentRef.current.scrollTop = 0;
}
}, [currentFile]);
return (
<div className="markdown-renderer" ref={contentRef}>
<div className="markdown-container">
{isLoading && (
<div className="loading-state">
<div className="loading-spinner"></div>
<p>加载中...</p>
</div>
)}
{error && !isLoading && (
<div className="error-state">
<div className="error-icon"></div>
<h2>加载失败</h2>
<p>{error}</p>
</div>
)}
{!isLoading && !error && !currentContent && (
<div className="empty-state" style={{ padding: '50px', textAlign: 'center', color: '#888' }}>
<p>请选择左侧文件查看内容</p>
<p style={{ fontSize: '0.8em', marginTop: '10px' }}>Select a file to view content</p>
</div>
)}
{!isLoading && !error && currentContent && (
<article className="markdown-body">
<ReactMarkdown
remarkPlugins={[
remarkGfm, // GitHub Flavored Markdown
remarkMath, // 数学公式支持
remarkBreaks, // 自动换行
]}
rehypePlugins={[
rehypeRaw, // 支持HTML标签
rehypeKatex, // 数学公式渲染
rehypeHighlight, // 代码高亮
]}
components={{
// 自定义图片渲染,支持点击放大
img: ({ node, ...props }) => (
<img
{...props}
loading="lazy"
onClick={(e) => {
// 可以添加图片预览功能
e.target.classList.toggle('zoomed');
}}
style={{ cursor: 'pointer' }}
/>
),
// 自定义链接渲染,外部链接在新标签页打开
a: ({ node, href, children, ...props }) => {
const isExternal = href?.startsWith('http');
return (
<a
href={href}
target={isExternal ? '_blank' : undefined}
rel={isExternal ? 'noopener noreferrer' : undefined}
{...props}
>
{children}
</a>
);
},
// 自定义代码块渲染
code: ({ node, inline, className, children, ...props }) => {
if (inline) {
return <code className={className} {...props}>{children}</code>;
}
return (
<code className={className} {...props}>
{children}
</code>
);
}
}}
>
{currentContent}
</ReactMarkdown>
{/* 显示文件元数据 */}
{currentMetadata && (
<div className="file-metadata">
<div className="metadata-divider"></div>
<div className="metadata-content">
<div className="metadata-item" title="字数统计">
<span className="metadata-icon">📝</span>
<span className="metadata-label">字数:</span>
<span className="metadata-value">{currentMetadata.wordCount}</span>
</div>
<div className="metadata-item" title="文件大小">
<span className="metadata-icon">💾</span>
<span className="metadata-label">大小:</span>
<span className="metadata-value">{formatFileSize(currentMetadata.fileSize)}</span>
</div>
{currentMetadata.createdTime && (
<div className="metadata-item" title="创建时间">
<span className="metadata-icon">📅</span>
<span className="metadata-label">创建于:</span>
<span className="metadata-value">{currentMetadata.createdTime}</span>
</div>
)}
{currentMetadata.modifiedTime && (
<div className="metadata-item" title="修改时间">
<span className="metadata-icon">🕒</span>
<span className="metadata-label">最后修改于:</span>
<span className="metadata-value">{currentMetadata.modifiedTime}</span>
</div>
)}
</div>
</div>
)}
</article>
)}
</div>
</div>
);
}
import React, { useEffect, useRef, useState } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import remarkBreaks from 'remark-breaks';
import rehypeKatex from 'rehype-katex';
import rehypeHighlight from 'rehype-highlight';
import rehypeRaw from 'rehype-raw';
import { useApp } from '../context/AppContext';
import './MarkdownRenderer.css';
import 'github-markdown-css/github-markdown-light.css';
import 'katex/dist/katex.min.css';
import 'highlight.js/styles/github.css';
function MarkdownRenderer() {
const { currentContent, currentFile, currentMetadata, isLoading, error } = useApp();
const contentRef = useRef(null);
const [copySuccess, setCopySuccess] = useState(false);
const [codeCopySuccess, setCodeCopySuccess] = useState({});
// 语言到文件扩展名的映射
const languageExtensions = {
javascript: 'js',
typescript: 'ts',
python: 'py',
java: 'java',
cpp: 'cpp',
c: 'c',
csharp: 'cs',
go: 'go',
rust: 'rs',
php: 'php',
ruby: 'rb',
swift: 'swift',
kotlin: 'kt',
scala: 'scala',
html: 'html',
css: 'css',
sql: 'sql',
bash: 'sh',
shell: 'sh',
json: 'json',
xml: 'xml',
yaml: 'yaml',
markdown: 'md',
dart: 'dart',
r: 'r',
matlab: 'm',
};
// 复制代码块
const handleCodeCopy = async (code, index) => {
try {
await navigator.clipboard.writeText(code);
setCodeCopySuccess({ ...codeCopySuccess, [index]: true });
setTimeout(() => {
setCodeCopySuccess({ ...codeCopySuccess, [index]: false });
}, 2000);
} catch (err) {
console.error('复制失败:', err);
}
};
// 下载代码块
const handleCodeDownload = (code, language) => {
const extension = languageExtensions[language?.toLowerCase()] || 'txt';
const blob = new Blob([code], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `code.${extension}`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
// 格式化文件大小
const formatFileSize = (bytes) => {
if (!bytes) return '0 B';
const kb = bytes / 1024;
if (kb < 1024) return `${kb.toFixed(2)} KB`;
const mb = kb / 1024;
if (mb < 1024) return `${mb.toFixed(2)} MB`;
const gb = mb / 1024;
return `${gb.toFixed(2)} GB`;
};
// 获取文件名(不带.md后缀
const getFileName = () => {
if (!currentFile) return '';
const fileName = currentFile.split('/').pop();
return fileName.replace(/\.md$/i, '');
};
// 复制内容到剪贴板
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(currentContent);
setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000);
} catch (err) {
console.error('复制失败:', err);
}
};
// 下载Markdown文件
const handleDownload = () => {
const blob = new Blob([currentContent], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${getFileName()}.md`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
// 当内容变化时,滚动到顶部
useEffect(() => {
if (contentRef.current) {
contentRef.current.scrollTop = 0;
}
// 重置复制状态
setCopySuccess(false);
setCodeCopySuccess({});
}, [currentFile]);
return (
<div className="markdown-renderer" ref={contentRef}>
<div className="markdown-container">
{isLoading && (
<div className="loading-state">
<div className="loading-spinner"></div>
<p>加载中...</p>
</div>
)}
{error && !isLoading && (
<div className="error-state">
<div className="error-icon"></div>
<h2>加载失败</h2>
<p>{error}</p>
</div>
)}
{!isLoading && !error && !currentContent && (
<div className="empty-state" style={{ padding: '50px', textAlign: 'center', color: '#888' }}>
<p>请选择左侧文件查看内容</p>
<p style={{ fontSize: '0.8em', marginTop: '10px' }}>Select a file to view content</p>
</div>
)}
{!isLoading && !error && currentContent && (
<>
{/* 文件头部:文件名 + 操作按钮 */}
<div className="file-header">
<h1 className="file-title">{getFileName()}</h1>
<div className="file-actions">
<button
className="action-button copy-button"
onClick={handleCopy}
title="复制内容"
>
{copySuccess ? '✓ 已复制' : '⧉ 复制'}
</button>
<button
className="action-button download-button"
onClick={handleDownload}
title="下载文件"
>
下载
</button>
</div>
</div>
<article className="markdown-body">
<ReactMarkdown
remarkPlugins={[
remarkGfm, // GitHub Flavored Markdown
remarkMath, // 数学公式支持
remarkBreaks, // 自动换行
]}
rehypePlugins={[
rehypeRaw, // 支持HTML标签
rehypeKatex, // 数学公式渲染
rehypeHighlight, // 代码高亮
]}
components={{
// 自定义图片渲染,支持点击放大
img: ({ node, ...props }) => (
<img
{...props}
loading="lazy"
onClick={(e) => {
// 可以添加图片预览功能
e.target.classList.toggle('zoomed');
}}
style={{ cursor: 'pointer' }}
/>
),
// 自定义链接渲染,外部链接在新标签页打开
a: ({ node, href, children, ...props }) => {
const isExternal = href?.startsWith('http');
return (
<a
href={href}
target={isExternal ? '_blank' : undefined}
rel={isExternal ? 'noopener noreferrer' : undefined}
{...props}
>
{children}
</a>
);
},
// 自定义代码块渲染 - DeepSeek 风格
pre: ({ node, children, ...props }) => {
// 获取 code 元素
const codeElement = children?.props;
const className = codeElement?.className || '';
const code = String(codeElement?.children || '').trim();
// 提取语言
const match = /language-(\w+)/.exec(className);
const language = match ? match[1] : 'text';
// 生成唯一索引
const codeIndex = `${language}-${code.substring(0, 20)}`;
return (
<div className="code-block-wrapper">
<div className="code-block-header">
<span className="code-language">{language}</span>
<div className="code-actions">
<button
className="code-action-btn"
onClick={() => handleCodeCopy(code, codeIndex)}
title="复制代码"
>
{codeCopySuccess[codeIndex] ? '✓ 已复制' : '⧉ 复制'}
</button>
<button
className="code-action-btn"
onClick={() => handleCodeDownload(code, language)}
title="下载代码"
>
下载
</button>
</div>
</div>
<pre {...props}>{children}</pre>
</div>
);
},
// 自定义行内代码
code: ({ node, inline, className, children, ...props }) => {
if (inline) {
return <code className={className} {...props}>{children}</code>;
}
return (
<code className={className} {...props}>
{children}
</code>
);
}
}}
>
{currentContent}
</ReactMarkdown>
{/* 显示文件元数据 */}
{currentMetadata && (
<div className="file-metadata">
<div className="metadata-divider"></div>
<div className="metadata-content">
<div className="metadata-item" title="字数统计">
<span className="metadata-icon">📝</span>
<span className="metadata-label">字数:</span>
<span className="metadata-value">{currentMetadata.wordCount}</span>
</div>
<div className="metadata-item" title="文件大小">
<span className="metadata-icon">💾</span>
<span className="metadata-label">大小:</span>
<span className="metadata-value">{formatFileSize(currentMetadata.fileSize)}</span>
</div>
{currentMetadata.createdTime && (
<div className="metadata-item" title="创建时间">
<span className="metadata-icon">📅</span>
<span className="metadata-label">创建于:</span>
<span className="metadata-value">{currentMetadata.createdTime}</span>
</div>
)}
{currentMetadata.modifiedTime && (
<div className="metadata-item" title="修改时间">
<span className="metadata-icon">🕒</span>
<span className="metadata-label">最后修改于:</span>
<span className="metadata-value">{currentMetadata.modifiedTime}</span>
</div>
)}
</div>
</div>
)}
</article>
</>
)}
</div>
</div>
);
}
export default MarkdownRenderer;

View File

@@ -22,7 +22,7 @@
color: var(--color-text);
box-shadow: none;
overflow: hidden; /* 防止内容溢出 */
font-family: 'Maple Mono CN', ui-monospace, var(--font-family-base);
font-family: 'LXGWWenKaiMono', ui-monospace, var(--font-family-base);
}
/* 侧边栏关闭状态 - 完全隐藏 */
@@ -54,7 +54,7 @@
letter-spacing: 0.02em; /* 字母间距 */
font-weight: 600;
color: var(--color-text);
font-family: 'Maple Mono CN', ui-monospace, var(--font-family-base);
font-family: 'LXGWWenKaiMono', ui-monospace, var(--font-family-base);
flex-shrink: 0; /* 标题不收缩 */
}
@@ -80,7 +80,7 @@
cursor: pointer;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 1px 3px rgba(31, 35, 40, 0.06);
font-family: 'Maple Mono CN', ui-monospace, var(--font-family-base);
font-family: 'LXGWWenKaiMono', ui-monospace, var(--font-family-base);
flex-shrink: 0; /* 按钮不收缩 */
margin-left: auto; /* 按钮靠右 */
opacity: 0.8;
@@ -222,7 +222,7 @@
padding: 2.5rem 1rem;
color: var(--color-muted);
font-size: 0.9rem;
font-family: 'Maple Mono CN', ui-monospace, var(--font-family-base);
font-family: 'LXGWWenKaiMono', ui-monospace, var(--font-family-base);
}
/* 加载动画 - 旋转圆圈 - GitHub 风格 */