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

@@ -0,0 +1,28 @@
# 萌芽笔记 PWA 说明
本前端已配置为 **PWA渐进式 Web 应用)**,支持:
- **安装到桌面/主屏**:浏览器中可“安装应用”,像原生应用一样打开
- **离线可用**:静态资源与部分 API 会被缓存,弱网/离线时可继续使用
- **独立窗口**:安装后以独立窗口运行(无浏览器地址栏)
## 图标
请将应用图标放在 **`public/logo.png`**。建议尺寸:
- 至少 **192×192** 像素(用于主屏图标)
- 若有 **512×512** 像素,可同时用于启动画面等
若未放置 `logo.png`PWA 仍可正常工作,但安装图标可能为默认或空白。
## 构建与部署
```bash
npm run build
```
构建产物在 `dist/`,部署后通过 **HTTPS** 访问即可使用 PWA 能力(本地预览可用 `npm run preview`)。
## 开发时
`vite.config.js` 中已开启 `devOptions: { enabled: true }`,开发时也会注册 Service Worker便于调试离线与安装行为。不需要时可改为 `enabled: false`

View File

@@ -1,13 +1,19 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>萌芽笔记</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="theme-color" content="#1a1a2e" />
<meta name="description" content="萌芽笔记 - Markdown 笔记 PWA 应用" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="萌芽笔记" />
<link rel="apple-touch-icon" href="/logo.png" />
<title>萌芽笔记</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -35,6 +35,7 @@
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.4.0",
"http-server": "^14.1.1",
"vite": "^7.1.7"
"vite": "^7.1.7",
"vite-plugin-pwa": "^1.2.0"
}
}

View File

@@ -29,8 +29,8 @@
--shadow-soft: 0 1px 3px rgba(31, 35, 40, 0.12), 0 8px 24px rgba(66, 74, 83, 0.12);
--radius-md: 6px; /* 中等圆角半径 - GitHub 标准 */
/* 字体系统 - Maple Mono CN 字体族 */
--font-family-base: 'Maple Mono CN', ui-monospace, -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
/* 字体系统 - LXGWWenKaiMono 字体族 */
--font-family-base: 'LXGWWenKaiMono', ui-monospace, -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
}
/* ========================================
@@ -40,7 +40,7 @@
/* 盒模型重置 - 确保所有元素使用 border-box */
* {
box-sizing: border-box;
font-family: 'Maple Mono CN', ui-monospace, var(--font-family-base);
font-family: 'LXGWWenKaiMono', ui-monospace, var(--font-family-base);
}
/* ========================================
@@ -77,8 +77,8 @@ body::-webkit-scrollbar {
.floating-open-button {
position: fixed;
top: 20px;
left: 20px;
top: 10px;
left: 10px;
z-index: 100;
display: flex;
align-items: center;
@@ -96,7 +96,7 @@ body::-webkit-scrollbar {
cursor: pointer;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 8px rgba(31, 35, 40, 0.08);
font-family: 'Maple Mono CN', ui-monospace, var(--font-family-base);
font-family: 'LXGWWenKaiMono', ui-monospace, var(--font-family-base);
opacity: 0.8;
}

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 风格 */

View File

@@ -8,9 +8,9 @@
======================================== */
@font-face {
font-family: 'Maple Mono CN';
src: url('/MapleMono-CN-ExtraBold.ttf') format('truetype');
font-weight: 800;
font-family: 'LXGWWenKaiMono';
src: url('/LXGWWenKaiMono-Medium.ttf') format('truetype');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@@ -21,7 +21,7 @@
/* 根元素字体和渲染优化 */
:root {
font-family: 'Maple Mono CN', ui-monospace, -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
font-family: 'LXGWWenKaiMono', ui-monospace, -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
font-size: 16px; /* 基础字体大小 - 16px 作为 1rem 基准 */
line-height: 1.6; /* 行高 - 提供良好的文本可读性 */
font-weight: 400; /* 默认字重 - 正常粗细 */
@@ -66,8 +66,8 @@ video {
/* 代码和预格式化文本的字体设置 */
code,
pre {
/* 等宽字体族 - 优先使用 Maple Mono CN */
font-family: 'Maple Mono CN', ui-monospace, 'Fira Code', 'JetBrains Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;
/* 等宽字体族 - 优先使用 LXGWWenKaiMono */
font-family: 'LXGWWenKaiMono', ui-monospace, 'Fira Code', 'JetBrains Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;
}
/* ========================================

View File

@@ -4,11 +4,20 @@ import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
import ErrorBoundary from './components/ErrorBoundary.jsx'
import { registerSW } from 'virtual:pwa-register'
console.log('[MengyaNote] 应用启动...');
console.log('[MengyaNote] 环境:', import.meta.env.MODE);
console.log('[MengyaNote] API地址:', import.meta.env.VITE_API_BASE || 'http://192.168.1.233:2424');
// PWA注册 Service Worker有新版本时自动更新
registerSW({
onNeedRefresh() {},
onOfflineReady() {
console.log('[MengyaNote] 内容已缓存,可离线使用');
}
});
createRoot(document.getElementById('root')).render(
<StrictMode>
<ErrorBoundary>

View File

@@ -1,190 +1,190 @@
// 文件节点类型
export const NODE_TYPES = {
FOLDER: 'folder',
FILE: 'file'
};
// 后端 API 基础地址(可以通过 Vite 环境变量覆盖)
// 如果为空,则使用相对路径(前后端同域名部署)
const API_BASE = import.meta.env.VITE_API_BASE || '';
const USE_RELATIVE_PATH = !API_BASE || API_BASE.trim() === '';
console.log('[MengyaNote] API配置:', {
API_BASE: API_BASE || '(相对路径)',
USE_RELATIVE_PATH
});
// 通用的fetch封装带超时和重试
async function fetchWithTimeout(url, options = {}, timeout = 10000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error('请求超时,请检查网络连接');
}
throw error;
}
}
// 从 FastAPI 后端读取目录树
export async function readDirectoryTree() {
const apiUrl = USE_RELATIVE_PATH ? '/api/tree' : `${API_BASE}/api/tree`;
console.log('[MengyaNote] 请求目录树:', apiUrl);
try {
const res = await fetchWithTimeout(apiUrl, {}, 15000);
if (!res.ok) {
throw new Error(`服务器返回错误: ${res.status} ${res.statusText}`);
}
const data = await res.json();
console.log('[MengyaNote] 目录树数据获取成功');
// 递归添加 isExpanded 属性,保持与原有前端逻辑兼容
function addExpandedProperty(nodes) {
return nodes.map(node => ({
...node,
isExpanded: false,
children: node.children ? addExpandedProperty(node.children) : []
}));
}
return addExpandedProperty(data);
} catch (error) {
console.error('[MengyaNote] 加载目录树失败:', error);
// 返回友好的错误提示
if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) {
throw new Error('无法连接到服务器,请检查:\n1. 后端服务是否运行\n2. API地址是否正确\n3. 网络连接是否正常');
}
throw new Error(`加载目录失败: ${error.message}`);
}
}
// 从 FastAPI 后端读取Markdown文件内容
export async function readMarkdownFile(relativePath) {
console.log('[MengyaNote] 请求文件:', relativePath);
try {
// 构建请求URL
let fetchUrl;
if (USE_RELATIVE_PATH) {
// 使用相对路径
const params = new URLSearchParams({ path: relativePath });
fetchUrl = `/api/file?${params.toString()}`;
} else {
// 使用绝对路径
const baseUrl = API_BASE.endsWith('/') ? API_BASE.slice(0, -1) : API_BASE;
const url = new URL(`${baseUrl}/api/file`);
url.searchParams.set('path', relativePath);
fetchUrl = url.toString();
}
console.log('[MengyaNote] 请求URL:', fetchUrl);
const res = await fetchWithTimeout(fetchUrl, {}, 15000);
console.log('[MengyaNote] 响应状态:', res.status);
if (!res.ok) {
if (res.status === 404) {
console.warn('[MengyaNote] 文件未找到:', relativePath);
return {
content: `# ${relativePath.split('/').pop().replace('.md', '')}
## 文件未找到
找不到文件:\`${relativePath}\`
可能的原因:
- 文件已被删除或移动
- 文件路径错误
- 后端服务未正确配置
请检查文件是否存在。`,
metadata: null
};
}
throw new Error(`加载文件失败: ${res.status} ${res.statusText}`);
}
const data = await res.json();
console.log('[MengyaNote] 文件内容加载成功, 大小:', data.content?.length, '字节');
return {
content: data.content || '',
metadata: {
wordCount: data.word_count || 0,
fileSize: data.file_size || 0,
createdTime: data.created_time || '',
modifiedTime: data.modified_time || ''
}
};
} catch (error) {
console.error('[MengyaNote] 加载文件内容失败:', error);
// 返回更友好的错误信息
const errorMessage = error.message.includes('请求超时')
? '文件加载超时,请重试'
: error.message.includes('Failed to fetch')
? '网络连接失败,请检查网络'
: error.message;
return {
content: `# 加载错误
## 无法加载文件内容
**错误信息:** ${errorMessage}
**请求路径:** \`${relativePath}\`
**解决方案:**
1. 检查网络连接是否正常
2. 确认后端服务正在运行
3. 刷新页面重试
4. 查看浏览器控制台获取详细错误信息`,
metadata: null
};
}
}
// 生成面包屑导航
export function generateBreadcrumbs(filePath) {
if (!filePath) return [];
const parts = filePath.split('/');
const breadcrumbs = [];
for (let i = 0; i < parts.length; i++) {
const name = parts[i];
const path = parts.slice(0, i + 1).join('/');
breadcrumbs.push({ name, path });
}
return breadcrumbs;
}
// 获取文件的标题(从文件名或内容中提取)
export function getFileTitle(filename, content = '') {
// 首先尝试从内容中提取第一个标题
const titleMatch = content.match(/^#\s+(.+)$/m);
if (titleMatch) {
return titleMatch[1].trim();
}
// 如果没有找到标题,使用文件名(去掉.md扩展名
return filename.replace(/\.md$/, '');
// 文件节点类型
export const NODE_TYPES = {
FOLDER: 'folder',
FILE: 'file'
};
// 后端 API 基础地址(可以通过 Vite 环境变量覆盖)
// 如果为空,则使用相对路径(前后端同域名部署)
const API_BASE = import.meta.env.VITE_API_BASE || '';
const USE_RELATIVE_PATH = !API_BASE || API_BASE.trim() === '';
console.log('[MengyaNote] API配置:', {
API_BASE: API_BASE || '(相对路径)',
USE_RELATIVE_PATH
});
// 通用的fetch封装带超时和重试
async function fetchWithTimeout(url, options = {}, timeout = 10000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error('请求超时,请检查网络连接');
}
throw error;
}
}
// 从 FastAPI 后端读取目录树
export async function readDirectoryTree() {
const apiUrl = USE_RELATIVE_PATH ? '/api/tree' : `${API_BASE}/api/tree`;
console.log('[MengyaNote] 请求目录树:', apiUrl);
try {
const res = await fetchWithTimeout(apiUrl, {}, 15000);
if (!res.ok) {
throw new Error(`服务器返回错误: ${res.status} ${res.statusText}`);
}
const data = await res.json();
console.log('[MengyaNote] 目录树数据获取成功');
// 递归添加 isExpanded 属性,保持与原有前端逻辑兼容
function addExpandedProperty(nodes) {
return nodes.map(node => ({
...node,
isExpanded: false,
children: node.children ? addExpandedProperty(node.children) : []
}));
}
return addExpandedProperty(data);
} catch (error) {
console.error('[MengyaNote] 加载目录树失败:', error);
// 返回友好的错误提示
if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) {
throw new Error('无法连接到服务器,请检查:\n1. 后端服务是否运行\n2. API地址是否正确\n3. 网络连接是否正常');
}
throw new Error(`加载目录失败: ${error.message}`);
}
}
// 从 FastAPI 后端读取Markdown文件内容
export async function readMarkdownFile(relativePath) {
console.log('[MengyaNote] 请求文件:', relativePath);
try {
// 构建请求URL
let fetchUrl;
if (USE_RELATIVE_PATH) {
// 使用相对路径
const params = new URLSearchParams({ path: relativePath });
fetchUrl = `/api/file?${params.toString()}`;
} else {
// 使用绝对路径
const baseUrl = API_BASE.endsWith('/') ? API_BASE.slice(0, -1) : API_BASE;
const url = new URL(`${baseUrl}/api/file`);
url.searchParams.set('path', relativePath);
fetchUrl = url.toString();
}
console.log('[MengyaNote] 请求URL:', fetchUrl);
const res = await fetchWithTimeout(fetchUrl, {}, 15000);
console.log('[MengyaNote] 响应状态:', res.status);
if (!res.ok) {
if (res.status === 404) {
console.warn('[MengyaNote] 文件未找到:', relativePath);
return {
content: `# ${relativePath.split('/').pop().replace('.md', '')}
## 文件未找到
找不到文件:\`${relativePath}\`
可能的原因:
- 文件已被删除或移动
- 文件路径错误
- 后端服务未正确配置
请检查文件是否存在。`,
metadata: null
};
}
throw new Error(`加载文件失败: ${res.status} ${res.statusText}`);
}
const data = await res.json();
console.log('[MengyaNote] 文件内容加载成功, 大小:', data.content?.length, '字节');
return {
content: data.content || '',
metadata: {
wordCount: data.word_count || 0,
fileSize: data.file_size || 0,
createdTime: data.created_time || '',
modifiedTime: data.modified_time || ''
}
};
} catch (error) {
console.error('[MengyaNote] 加载文件内容失败:', error);
// 返回更友好的错误信息
const errorMessage = error.message.includes('请求超时')
? '文件加载超时,请重试'
: error.message.includes('Failed to fetch')
? '网络连接失败,请检查网络'
: error.message;
return {
content: `# 加载错误
## 无法加载文件内容
**错误信息:** ${errorMessage}
**请求路径:** \`${relativePath}\`
**解决方案:**
1. 检查网络连接是否正常
2. 确认后端服务正在运行
3. 刷新页面重试
4. 查看浏览器控制台获取详细错误信息`,
metadata: null
};
}
}
// 生成面包屑导航
export function generateBreadcrumbs(filePath) {
if (!filePath) return [];
const parts = filePath.split('/');
const breadcrumbs = [];
for (let i = 0; i < parts.length; i++) {
const name = parts[i];
const path = parts.slice(0, i + 1).join('/');
breadcrumbs.push({ name, path });
}
return breadcrumbs;
}
// 获取文件的标题(从文件名或内容中提取)
export function getFileTitle(filename, content = '') {
// 首先尝试从内容中提取第一个标题
const titleMatch = content.match(/^#\s+(.+)$/m);
if (titleMatch) {
return titleMatch[1].trim();
}
// 如果没有找到标题,使用文件名(去掉.md扩展名
return filename.replace(/\.md$/, '');
}

View File

@@ -1,8 +1,68 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
plugins: [react()],
plugins: [
react(),
VitePWA({
registerType: 'autoUpdate', // 发现新版本时自动更新
includeAssets: ['logo.png'],
manifest: {
name: '萌芽笔记',
short_name: '萌芽笔记',
description: '萌芽笔记 - Markdown 笔记 PWA',
theme_color: '#1a1a2e',
background_color: '#16213e',
display: 'standalone',
orientation: 'portrait-primary',
scope: '/',
start_url: './',
icons: [
{
src: 'logo.png',
sizes: '192x192',
type: 'image/png',
purpose: 'any'
},
{
src: 'logo.png',
sizes: '512x512',
type: 'image/png',
purpose: 'any'
},
{
src: 'logo.png',
sizes: '192x192',
type: 'image/png',
purpose: 'maskable'
},
{
src: 'logo.png',
sizes: '512x512',
type: 'image/png',
purpose: 'maskable'
}
]
},
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
runtimeCaching: [
{
urlPattern: /^https?:\/\/.*\/api\/.*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: { maxEntries: 32, maxAgeSeconds: 24 * 60 * 60 },
networkTimeoutSeconds: 10,
cacheableResponse: { statuses: [0, 200] }
}
}
]
},
devOptions: { enabled: true } // 开发时也启用 PWA 便于调试
})
],
base: './', // Ensure relative paths for assets
server: {
host: '0.0.0.0', // 监听所有网络接口(包括局域网)