chore: sync local changes (2026-03-12)
This commit is contained in:
28
mengyanote-frontend/PWA.md
Normal file
28
mengyanote-frontend/PWA.md
Normal 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`。
|
||||
@@ -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>
|
||||
|
||||
4146
mengyanote-frontend/package-lock.json
generated
4146
mengyanote-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>更换其他浏览器(推荐 Chrome、Edge、Firefox)</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>更换其他浏览器(推荐 Chrome、Edge、Firefox)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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 风格 */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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$/, '');
|
||||
}
|
||||
@@ -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', // 监听所有网络接口(包括局域网)
|
||||
|
||||
Reference in New Issue
Block a user