main
mula.liu 2025-12-25 12:22:35 +08:00
parent 535350fd6f
commit 00253cd2e7
105 changed files with 13034 additions and 790 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

View File

@ -166,7 +166,7 @@ NEX Docus/
│ │ └── init_db.py # 数据库初始化 │ │ └── init_db.py # 数据库初始化
│ ├── Dockerfile # 后端镜像 │ ├── Dockerfile # 后端镜像
│ └── requirements.txt # Python 依赖 │ └── requirements.txt # Python 依赖
├── forntend/ # 前端代码 ├── frontend/ # 前端代码
│ ├── src/ # 源代码 │ ├── src/ # 源代码
│ ├── Dockerfile # 前端镜像 │ ├── Dockerfile # 前端镜像
│ └── nginx.conf # Nginx 配置 │ └── nginx.conf # Nginx 配置

View File

@ -55,7 +55,7 @@ python main.py
```bash ```bash
# 1. 打开新终端,进入前端目录 # 1. 打开新终端,进入前端目录
cd forntend cd frontend
# 2. 安装依赖 # 2. 安装依赖
npm install npm install
@ -110,7 +110,7 @@ NEX Docus/
│ ├── main.py # 应用入口 │ ├── main.py # 应用入口
│ └── requirements.txt # 依赖包 │ └── requirements.txt # 依赖包
├── forntend/ # 前端应用React + Vite ├── frontend/ # 前端应用React + Vite
│ ├── src/ │ ├── src/
│ │ ├── api/ # API 请求 │ │ ├── api/ # API 请求
│ │ ├── components/ # 通用组件 │ │ ├── components/ # 通用组件
@ -159,7 +159,7 @@ pip install -r requirements.txt
**解决**: **解决**:
- 确认后端服务已启动 - 确认后端服务已启动
- 检查 `forntend/.env` 中的 API 地址配置 - 检查 `frontend/.env` 中的 API 地址配置
- 检查浏览器控制台的网络请求 - 检查浏览器控制台的网络请求
### 4. 文件上传/保存失败 ### 4. 文件上传/保存失败
@ -216,7 +216,7 @@ pip install -r requirements.txt
cd backend && source venv/bin/activate && python main.py cd backend && source venv/bin/activate && python main.py
# 前端 # 前端
cd forntend && npm run dev cd frontend && npm run dev
# 数据库初始化 # 数据库初始化
mysql -h10.100.51.51 -uroot -pUnis@321 < backend/scripts/init_database.sql mysql -h10.100.51.51 -uroot -pUnis@321 < backend/scripts/init_database.sql

View File

@ -61,7 +61,7 @@ NEX Docus/
│ ├── scripts/ # 初始化脚本 │ ├── scripts/ # 初始化脚本
│ └── main.py # 应用入口 │ └── main.py # 应用入口
├── forntend/ # React 前端应用 ├── frontend/ # React 前端应用
│ ├── src/ │ ├── src/
│ │ ├── api/ # API 封装 │ │ ├── api/ # API 封装
│ │ ├── components/ # 通用组件 │ │ ├── components/ # 通用组件
@ -107,7 +107,7 @@ python main.py
### 3. 启动前端 ### 3. 启动前端
```bash ```bash
cd forntend cd frontend
npm install npm install
npm run dev npm run dev
``` ```
@ -213,9 +213,9 @@ npm run dev
### 前端开发 ### 前端开发
1. 在 `forntend/src/api/` 封装 API 请求 1. 在 `frontend/src/api/` 封装 API 请求
2. 在 `forntend/src/pages/` 创建页面组件 2. 在 `frontend/src/pages/` 创建页面组件
3. 在 `forntend/src/App.jsx` 添加路由 3. 在 `frontend/src/App.jsx` 添加路由
4. 使用 Zustand 管理全局状态 4. 使用 Zustand 管理全局状态
--- ---

View File

@ -7,7 +7,7 @@
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Docker](https://img.shields.io/badge/Docker-Ready-brightgreen.svg)](DEPLOY.md) [![Docker](https://img.shields.io/badge/Docker-Ready-brightgreen.svg)](DEPLOY.md)
[![Python](https://img.shields.io/badge/Python-3.9+-blue.svg)](backend/) [![Python](https://img.shields.io/badge/Python-3.9+-blue.svg)](backend/)
[![React](https://img.shields.io/badge/React-18.2-61DAFB.svg)](forntend/) [![React](https://img.shields.io/badge/React-18.2-61DAFB.svg)](frontend/)
</div> </div>
@ -90,7 +90,7 @@ API 文档: `http://localhost:8000/docs`
#### 前端开发 #### 前端开发
```bash ```bash
cd forntend cd frontend
# 安装依赖 # 安装依赖
npm install npm install
@ -156,7 +156,7 @@ NEX Docus/
│ ├── scripts/ # 脚本文件 │ ├── scripts/ # 脚本文件
│ ├── Dockerfile # 后端镜像 │ ├── Dockerfile # 后端镜像
│ └── requirements.txt # Python 依赖 │ └── requirements.txt # Python 依赖
├── forntend/ # 前端应用 ├── frontend/ # 前端应用
│ ├── src/ │ ├── src/
│ │ ├── api/ # API 请求 │ │ ├── api/ # API 请求
│ │ ├── components/ # 组件 │ │ ├── components/ # 组件

View File

@ -136,7 +136,7 @@ def create_project():
print("="*60) print("="*60)
print("\n你现在可以:") print("\n你现在可以:")
print("1. 启动后端服务: cd backend && python main.py") print("1. 启动后端服务: cd backend && python main.py")
print("2. 启动前端服务: cd forntend && npm run dev") print("2. 启动前端服务: cd frontend && npm run dev")
print("3. 登录系统 (admin / admin@123)") print("3. 登录系统 (admin / admin@123)")
print("4. 在项目中添加你的文档") print("4. 在项目中添加你的文档")
print() print()

View File

@ -87,7 +87,7 @@ services:
# 前端服务 # 前端服务
frontend: frontend:
build: build:
context: ./forntend context: ./frontend
dockerfile: Dockerfile dockerfile: Dockerfile
args: args:
- VITE_API_BASE_URL=${VITE_API_BASE_URL:-http://localhost:8000} - VITE_API_BASE_URL=${VITE_API_BASE_URL:-http://localhost:8000}

View File

@ -1,97 +0,0 @@
.document-editor-container {
height: calc(100vh - 64px);
}
.document-sider {
border-right: 1px solid #f0f0f0;
overflow-y: auto;
}
.sider-header {
padding: 16px;
border-bottom: 1px solid #f0f0f0;
display: flex;
flex-direction: column;
gap: 12px;
}
.sider-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.sider-actions {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.sider-actions .ant-btn {
color: rgba(0, 0, 0, 0.65);
background: #f5f5f5;
border: 1px solid #e8e8e8;
}
.sider-actions .ant-btn:hover {
color: #1890ff;
background: #e6f7ff;
border-color: #91d5ff;
}
.file-tree {
padding: 8px;
}
.document-content {
background: white;
display: flex;
flex-direction: column;
}
.content-header {
padding: 16px 24px;
border-bottom: 1px solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
background: white;
flex-shrink: 0;
}
.content-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.editor-container {
flex: 1;
padding: 16px 24px;
overflow: auto;
display: flex;
flex-direction: column;
}
.editor-container > div {
flex: 1;
display: flex;
flex-direction: column;
}
.empty-editor {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #999;
}
/* Markdown 编辑器样式覆盖 */
.w-md-editor {
flex: 1 !important;
}
.w-md-editor-content {
height: 100% !important;
}

View File

@ -16,7 +16,7 @@ NEX Docus 前端项目 - 基于 React + Vite + Ant Design 构建。
## 项目结构 ## 项目结构
``` ```
forntend/ frontend/
├── public/ # 静态资源 ├── public/ # 静态资源
├── src/ ├── src/
│ ├── api/ # API 请求封装 │ ├── api/ # API 请求封装

View File

@ -2,7 +2,7 @@
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NEX Docus - 文档管理平台</title> <title>NEX Docus - 文档管理平台</title>
</head> </head>

11104
frontend/package-lock.json generated 100644

File diff suppressed because it is too large Load Diff

View File

@ -11,9 +11,16 @@
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.2.6", "@ant-design/icons": "^5.2.6",
"@bytemd/plugin-breaks": "^1.22.0",
"@bytemd/plugin-frontmatter": "^1.22.0",
"@bytemd/plugin-gemoji": "^1.22.0",
"@bytemd/plugin-gfm": "^1.22.0",
"@bytemd/plugin-highlight": "^1.22.0",
"@bytemd/react": "^1.22.0",
"@uiw/react-md-editor": "^4.0.4", "@uiw/react-md-editor": "^4.0.4",
"antd": "^5.12.0", "antd": "^5.12.0",
"axios": "^1.6.2", "axios": "^1.6.2",
"bytemd": "^1.22.0",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",

View File

@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<!-- Background circle -->
<circle cx="32" cy="32" r="30" fill="#1890ff"/>
<!-- Document icon -->
<g transform="translate(16, 14)">
<!-- Main document shape -->
<path d="M4 0 L24 0 L32 8 L32 36 L4 36 Z" fill="#ffffff" opacity="0.95"/>
<!-- Folded corner -->
<path d="M24 0 L24 8 L32 8 Z" fill="#ffffff" opacity="0.7"/>
<!-- Text lines -->
<rect x="8" y="14" width="16" height="2" fill="#1890ff" rx="1"/>
<rect x="8" y="20" width="16" height="2" fill="#1890ff" rx="1"/>
<rect x="8" y="26" width="12" height="2" fill="#1890ff" rx="1"/>
</g>
<!-- Small "N" badge -->
<circle cx="48" cy="48" r="10" fill="#52c41a"/>
<text x="48" y="53" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#ffffff" text-anchor="middle">N</text>
</svg>

After

Width:  |  Height:  |  Size: 875 B

View File

@ -1,4 +1,4 @@
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' import { BrowserRouter, Routes, Route, Navigate, useParams } from 'react-router-dom'
import { ConfigProvider } from 'antd' import { ConfigProvider } from 'antd'
import zhCN from 'antd/locale/zh_CN' import zhCN from 'antd/locale/zh_CN'
import Login from '@/pages/Login/Login' import Login from '@/pages/Login/Login'
@ -16,6 +16,12 @@ import Roles from '@/pages/System/Roles'
import ProtectedRoute from '@/components/ProtectedRoute' import ProtectedRoute from '@/components/ProtectedRoute'
import '@/App.css' import '@/App.css'
//
function RedirectToDocs() {
const { projectId } = useParams()
return <Navigate to={`/projects/${projectId}/docs`} replace />
}
function App() { function App() {
return ( return (
<ConfigProvider locale={zhCN}> <ConfigProvider locale={zhCN}>
@ -66,6 +72,8 @@ function App() {
</ProtectedRoute> </ProtectedRoute>
} }
/> />
{/* 捕获所有 /projects/:projectId/* 路径(包括中文路径),重定向到文档页面 */}
<Route path="/projects/:projectId/*" element={<RedirectToDocs />} />
{/* 功能开发中页面 */} {/* 功能开发中页面 */}
<Route <Route
path="/constructing" path="/constructing"

View File

@ -17,10 +17,13 @@ export function getProjectTree(projectId) {
* 获取文件内容 * 获取文件内容
*/ */
export function getFileContent(projectId, path) { export function getFileContent(projectId, path) {
// 直接在 URL 中拼接参数,避免 axios 自动编码
// 手动编码一次,确保不会双重编码
const encodedPath = encodeURIComponent(path)
return request({ return request({
url: `/files/${projectId}/file`, url: `/files/${projectId}/file?path=${encodedPath}`,
method: 'get', method: 'get',
params: { path },
}) })
} }

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -1,9 +1,8 @@
import { Layout, Input, Badge, Avatar, Dropdown, Space } from 'antd' import { Layout, Badge, Avatar, Dropdown, Space } from 'antd'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { import {
MenuFoldOutlined, MenuFoldOutlined,
MenuUnfoldOutlined, MenuUnfoldOutlined,
SearchOutlined,
BellOutlined, BellOutlined,
QuestionCircleOutlined, QuestionCircleOutlined,
FileTextOutlined, FileTextOutlined,
@ -81,16 +80,8 @@ function AppHeader({ collapsed, onToggle }) {
</div> </div>
</div> </div>
{/* 右侧:搜索 + 功能按钮 + 用户信息 */} {/* 右侧:功能按钮 + 用户信息 */}
<div className="header-right"> <div className="header-right">
{/* 搜索框 */}
<Input
className="header-search"
placeholder="搜索..."
prefix={<SearchOutlined />}
style={{ width: 200 }}
/>
{/* 功能图标 */} {/* 功能图标 */}
<Space size={16} className="header-actions"> <Space size={16} className="header-actions">
{/* 动态渲染 header 菜单 */} {/* 动态渲染 header 菜单 */}

View File

@ -0,0 +1,309 @@
/* 覆盖MainLayout的content-wrapper padding */
.document-editor-page {
margin: -16px;
height: calc(100vh - 64px);
width: calc(100% + 32px);
display: flex;
}
.document-editor-container {
height: 100%;
width: 100%;
display: flex;
flex: 1;
}
.document-sider {
border-right: 1px solid #f0f0f0;
overflow-y: auto;
flex-shrink: 0;
}
.sider-header {
padding: 16px;
border-bottom: 1px solid #f0f0f0;
display: flex;
flex-direction: column;
gap: 12px;
}
.sider-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.sider-actions {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.sider-actions .ant-btn {
color: rgba(0, 0, 0, 0.65);
background: #f5f5f5;
border: 1px solid #e8e8e8;
}
.sider-actions .ant-btn:hover {
color: #1890ff;
background: #e6f7ff;
border-color: #91d5ff;
}
.file-tree {
padding: 8px;
}
.document-content {
height: 100%;
width: 100%;
background: white;
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
}
/* 强制覆盖 Ant Design Content 的默认样式 */
.document-editor-container .ant-layout-content {
display: flex !important;
flex-direction: column !important;
flex: 1 !important;
min-width: 0 !important;
width: 100% !important;
}
.content-header {
padding: 16px 24px;
border-bottom: 1px solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
background: white;
flex-shrink: 0;
}
.editor-container {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
min-width: 0;
overflow: hidden;
width: 100%;
}
.bytemd-wrapper {
flex: 1;
display: flex;
flex-direction: column; /* Changed to column to force child stretch width */
min-height: 0;
min-width: 0;
overflow: hidden;
width: 100%;
height: 100%;
padding: 5px 10px;
}
/* Fix for bytemd-react wrapper div */
.bytemd-wrapper > div {
flex: 1;
display: flex;
flex-direction: column;
width: 100% !important;
height: 100% !important;
min-width: 0;
}
.empty-editor {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #999;
}
/* ByteMD 编辑器样式覆盖 */
.bytemd {
width: 100% !important;
height: 100% !important;
display: flex !important;
flex-direction: column !important;
border: 1px solid #d9d9d9;
border-radius: 2px;
overflow: hidden;
max-width: none !important; /* Ensure no max-width constraint */
box-sizing: border-box;
}
/* 工具栏样式 */
.bytemd-toolbar {
flex-shrink: 0;
border-bottom: 1px solid #d9d9d9;
background-color: #fafafa;
box-sizing: border-box; /* Added for consistent box model */
}
/* 编辑和预览区域容器 */
.bytemd-body {
flex: 1 !important;
display: flex !important;
overflow: hidden;
min-height: 0;
width: 100% !important;
box-sizing: border-box; /* Added for consistent box model */
min-width: 0; /* Prevent flex item from overflowing */
}
/* 编辑区域 - 固定50%宽度 */
.bytemd-editor {
width: 50% !important;
flex: 0 0 50% !important;
display: flex !important;
flex-direction: column !important;
overflow: hidden;
min-height: 0;
max-width: 50% !important;
box-sizing: border-box; /* Added for consistent box model */
min-width: 0; /* Prevent flex item from overflowing */
}
/* 预览区域 - 固定50%宽度 */
.bytemd-preview {
width: 50% !important;
flex: 0 0 50% !important;
overflow-y: auto !important;
overflow-x: hidden !important;
padding: 16px;
font-size: 14px;
line-height: 1.8;
max-width: 50% !important;
box-sizing: border-box; /* Added for consistent box model */
min-width: 0; /* Prevent flex item from overflowing */
}
/* CodeMirror 容器 */
.bytemd-editor .CodeMirror {
flex: 1;
min-height: 0;
font-size: 14px;
line-height: 1.8;
width: 100%;
height: 100%;
}
/* CodeMirror 滚动容器 */
.bytemd-editor .CodeMirror-scroll {
overflow-y: scroll !important;
overflow-x: auto !important;
min-height: 100% !important;
height: 100% !important;
}
/* 预览区域的markdown样式 */
.bytemd-preview h1,
.bytemd-preview h2,
.bytemd-preview h3,
.bytemd-preview h4,
.bytemd-preview h5,
.bytemd-preview h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
}
.bytemd-preview h1 {
font-size: 2em;
border-bottom: 1px solid #eaecef;
padding-bottom: 0.3em;
}
.bytemd-preview h2 {
font-size: 1.5em;
border-bottom: 1px solid #eaecef;
padding-bottom: 0.3em;
}
.bytemd-preview p {
margin-bottom: 16px;
}
.bytemd-preview code {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
background-color: rgba(27, 31, 35, 0.05);
border-radius: 3px;
}
.bytemd-preview pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #f6f8fa;
border-radius: 3px;
margin-bottom: 16px;
}
.bytemd-preview pre code {
display: inline;
padding: 0;
margin: 0;
overflow: visible;
line-height: inherit;
background-color: transparent;
border: 0;
}
.bytemd-preview img {
max-width: 100%;
box-sizing: content-box;
}
.bytemd-preview table {
border-collapse: collapse;
border-spacing: 0;
margin-bottom: 16px;
}
.bytemd-preview table th,
.bytemd-preview table td {
padding: 6px 13px;
border: 1px solid #dfe2e5;
}
.bytemd-preview table th {
font-weight: 600;
background-color: #f6f8fa;
}
.bytemd-preview blockquote {
margin: 0 0 16px;
padding: 0 1em;
color: #6a737d;
border-left: 0.25em solid #dfe2e5;
}
.bytemd-preview ul,
.bytemd-preview ol {
padding-left: 2em;
margin-bottom: 16px;
}
.bytemd-preview li {
margin-bottom: 0.25em;
}
/* 文件名过长时显示省略号,不折行 */
.content-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 600px;
}

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from 'react' import { useState, useEffect, useRef, useMemo } from 'react'
import { useParams, useNavigate } from 'react-router-dom' import { useParams, useNavigate } from 'react-router-dom'
import { Layout, Tree, Button, message, Modal, Input, Space, Tooltip, Dropdown, Upload, Select } from 'antd' import { Layout, Tree, Button, message, Modal, Input, Space, Tooltip, Dropdown, Upload, Select } from 'antd'
import { import {
@ -16,7 +16,14 @@ import {
SwapOutlined, SwapOutlined,
FileImageOutlined, FileImageOutlined,
} from '@ant-design/icons' } from '@ant-design/icons'
import MDEditor, { commands } from '@uiw/react-md-editor' import { Editor } from '@bytemd/react'
import gfm from '@bytemd/plugin-gfm'
import highlight from '@bytemd/plugin-highlight'
import breaks from '@bytemd/plugin-breaks'
import frontmatter from '@bytemd/plugin-frontmatter'
import gemoji from '@bytemd/plugin-gemoji'
import 'bytemd/dist/index.css'
import 'highlight.js/styles/github.css'
import { import {
getProjectTree, getProjectTree,
getFileContent, getFileContent,
@ -35,7 +42,6 @@ function DocumentEditor() {
const { projectId } = useParams() const { projectId } = useParams()
const navigate = useNavigate() const navigate = useNavigate()
const fileInputRef = useRef(null) const fileInputRef = useRef(null)
const imageInputRef = useRef(null)
const [treeData, setTreeData] = useState([]) const [treeData, setTreeData] = useState([])
const [selectedFile, setSelectedFile] = useState(null) const [selectedFile, setSelectedFile] = useState(null)
const [selectedNode, setSelectedNode] = useState(null) // const [selectedNode, setSelectedNode] = useState(null) //
@ -461,6 +467,49 @@ function DocumentEditor() {
} }
} }
// ByteMD
const plugins = useMemo(() => {
//
const uploadImagesPlugin = {
actions: [
{
title: '上传图片',
icon: '<svg width="16" height="16" viewBox="0 0 16 16"><path fill="currentColor" d="M14.998 2l.002.002v11.996l-.002.002H1.002L1 13.998V2.002L1.002 2h13.996zM15 1H1c-.55 0-1 .45-1 1v12c0 .55.45 1 1 1h14c.55 0 1-.45 1-1V2c0-.55-.45-1-1-1z"/><path fill="currentColor" d="M13 4.5a1.5 1.5 0 1 1-3.001-.001A1.5 1.5 0 0 1 13 4.5zM14 13H2v-2l3.5-6 4 5h1L14 7z"/></svg>',
handler: {
type: 'action',
click: (ctx) => {
const input = document.createElement('input')
input.type = 'file'
input.accept = 'image/*'
input.onchange = async (e) => {
const file = e.target.files?.[0]
if (file) {
message.loading('上传图片中...', 0)
const url = await handleImageUpload(file)
message.destroy()
if (url) {
ctx.appendBlock(`![image](${url})`)
message.success('图片上传成功')
}
}
}
input.click()
},
},
},
],
}
return [
gfm(),
highlight(),
breaks(),
frontmatter(),
gemoji(),
uploadImagesPlugin,
]
}, [projectId])
// //
const handlePaste = async (event) => { const handlePaste = async (event) => {
const items = event.clipboardData?.items const items = event.clipboardData?.items
@ -510,53 +559,6 @@ function DocumentEditor() {
} }
} }
//
const handleSelectLocalImage = () => {
imageInputRef.current?.click()
}
//
const handleLocalImageChange = async (event) => {
const file = event.target.files?.[0]
if (!file) return
if (!file.type.startsWith('image/')) {
message.warning('请选择图片文件')
return
}
message.loading('上传图片中...', 0)
const url = await handleImageUpload(file)
message.destroy()
if (url) {
const imageMarkdown = `![${file.name}](${url})`
setFileContent(prev => prev + '\n' + imageMarkdown)
message.success('图片上传成功')
}
// input
event.target.value = ''
}
//
const addLocalImageCommand = {
name: 'addLocalImage',
keyCommand: 'addLocalImage',
buttonProps: { 'aria-label': 'Add local image' },
icon: (
<svg width="12" height="12" viewBox="0 0 20 20">
<path
fill="currentColor"
d="M15 9c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm4-7H1c-.55 0-1 .45-1 1v14c0 .55.45 1 1 1h18c.55 0 1-.45 1-1V3c0-.55-.45-1-1-1zm-1 13l-6-5-2 2-4-5-4 8V4h16v11z"
/>
</svg>
),
execute: () => {
handleSelectLocalImage()
},
}
const renderTreeIcon = ({ isLeaf }) => { const renderTreeIcon = ({ isLeaf }) => {
return isLeaf ? <FileOutlined /> : <FolderOutlined /> return isLeaf ? <FileOutlined /> : <FolderOutlined />
} }
@ -602,7 +604,8 @@ function DocumentEditor() {
return ( return (
<MainLayout> <MainLayout>
<Layout className="document-editor-container"> <div className="document-editor-page">
<Layout className="document-editor-container">
<Sider <Sider
width={280} width={280}
theme="light" theme="light"
@ -694,39 +697,25 @@ function DocumentEditor() {
</Space> </Space>
</div> </div>
<div className="editor-container" data-color-mode="light"> <div className="editor-container">
{selectedFile ? ( {selectedFile ? (
<div onPaste={handlePaste} onDrop={handleDrop} onDragOver={(e) => e.preventDefault()}> <div
<MDEditor className="bytemd-wrapper"
onPaste={handlePaste}
onDrop={handleDrop}
onDragOver={(e) => e.preventDefault()}
>
<Editor
key={selectedFile}
value={fileContent} value={fileContent}
onChange={setFileContent} onChange={(v) => setFileContent(v)}
height={editorHeight} plugins={plugins}
preview="live" locale={{
commands={[ en: {
commands.bold, 'Write': '编辑',
commands.italic, 'Preview': '预览',
commands.strikethrough, },
commands.hr, }}
commands.title,
commands.divider,
commands.link,
commands.quote,
commands.code,
commands.image,
addLocalImageCommand,
commands.divider,
commands.unorderedListCommand,
commands.orderedListCommand,
commands.checkedListCommand,
]}
/>
{/* 隐藏的图片选择input */}
<input
ref={imageInputRef}
type="file"
accept="image/*"
style={{ display: 'none' }}
onChange={handleLocalImageChange}
/> />
</div> </div>
) : ( ) : (
@ -737,6 +726,7 @@ function DocumentEditor() {
</div> </div>
</Content> </Content>
</Layout> </Layout>
</div>
<Modal <Modal
title={ title={

View File

@ -100,6 +100,7 @@
.toc-content .ant-anchor { .toc-content .ant-anchor {
padding-left: 0; padding-left: 0;
padding-bottom: 65px; /* 给Anchor组件添加底部内边距避免最后一项被遮挡 */
} }
.toc-content .ant-anchor-link { .toc-content .ant-anchor-link {

View File

@ -162,31 +162,52 @@ function DocumentPage() {
// markdown // markdown
const handleMarkdownLink = (e, href) => { const handleMarkdownLink = (e, href) => {
// .md //
if (href && href.endsWith('.md') && !href.startsWith('http')) { if (!href || href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//')) {
e.preventDefault() return //
//
const targetPath = resolveRelativePath(selectedFile, href)
//
const parentPath = targetPath.substring(0, targetPath.lastIndexOf('/'))
if (parentPath && !openKeys.includes(parentPath)) {
//
const pathParts = parentPath.split('/')
const allParentPaths = []
let currentPath = ''
for (const part of pathParts) {
currentPath = currentPath ? `${currentPath}/${part}` : part
allParentPaths.push(currentPath)
}
setOpenKeys([...new Set([...openKeys, ...allParentPaths])])
}
//
setSelectedFile(targetPath)
loadMarkdown(targetPath)
} }
//
if (href.startsWith('#')) {
return //
}
//
e.preventDefault()
// .md
if (!href.endsWith('.md')) {
return
}
// href Markdown URL
let decodedHref = href
try {
decodedHref = decodeURIComponent(href)
} catch (e) {
console.warn('href 解码失败,使用原始值:', href)
}
//
const targetPath = resolveRelativePath(selectedFile, decodedHref)
//
const parentPath = targetPath.substring(0, targetPath.lastIndexOf('/'))
if (parentPath && !openKeys.includes(parentPath)) {
//
const pathParts = parentPath.split('/')
const allParentPaths = []
let currentPath = ''
for (const part of pathParts) {
currentPath = currentPath ? `${currentPath}/${part}` : part
allParentPaths.push(currentPath)
}
setOpenKeys([...new Set([...openKeys, ...allParentPaths])])
}
//
setSelectedFile(targetPath)
loadMarkdown(targetPath)
} }
// //

Some files were not shown because too many files have changed in this diff Show More