0.9.3
parent
535350fd6f
commit
00253cd2e7
Binary file not shown.
|
After Width: | Height: | Size: 393 KiB |
|
|
@ -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 配置
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
10
README.md
10
README.md
|
|
@ -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 管理全局状态
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](DEPLOY.md)
|
[](DEPLOY.md)
|
||||||
[](backend/)
|
[](backend/)
|
||||||
[](forntend/)
|
[](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/ # 组件
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -16,7 +16,7 @@ NEX Docus 前端项目 - 基于 React + Vite + Ant Design 构建。
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
```
|
```
|
||||||
forntend/
|
frontend/
|
||||||
├── public/ # 静态资源
|
├── public/ # 静态资源
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── api/ # API 请求封装
|
│ ├── api/ # API 请求封装
|
||||||
|
|
@ -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>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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",
|
||||||
|
|
@ -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 |
|
|
@ -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"
|
||||||
|
|
@ -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 },
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
|
@ -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 菜单 */}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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(``)
|
||||||
|
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 = ``
|
|
||||||
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,6 +604,7 @@ function DocumentEditor() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
|
<div className="document-editor-page">
|
||||||
<Layout className="document-editor-container">
|
<Layout className="document-editor-container">
|
||||||
<Sider
|
<Sider
|
||||||
width={280}
|
width={280}
|
||||||
|
|
@ -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={
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -162,12 +162,34 @@ 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('//')) {
|
||||||
|
return // 外部链接,允许默认行为
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是锚点链接
|
||||||
|
if (href.startsWith('#')) {
|
||||||
|
return // 锚点链接,允许默认行为
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他所有链接都视为内部文档链接,阻止默认跳转
|
||||||
e.preventDefault()
|
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, href)
|
const targetPath = resolveRelativePath(selectedFile, decodedHref)
|
||||||
|
|
||||||
// 自动展开父目录
|
// 自动展开父目录
|
||||||
const parentPath = targetPath.substring(0, targetPath.lastIndexOf('/'))
|
const parentPath = targetPath.substring(0, targetPath.lastIndexOf('/'))
|
||||||
|
|
@ -187,7 +209,6 @@ function DocumentPage() {
|
||||||
setSelectedFile(targetPath)
|
setSelectedFile(targetPath)
|
||||||
loadMarkdown(targetPath)
|
loadMarkdown(targetPath)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 进入编辑模式
|
// 进入编辑模式
|
||||||
const handleEdit = () => {
|
const handleEdit = () => {
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue