Skip to content

VitePress 在线编辑功能开发完整教程

前言

这个教程将指导您为 VitePress 笔记网站添加在线编辑功能,包括:

  • 用户认证系统
  • 汉堡菜单界面
  • Markdown 在线编辑器
  • 版本控制和冲突检测
  • 草稿箱功能

重要提醒: 在开始之前,请确保备份您当前的工作,充分利用 Git 版本控制来保护代码。

目录

  1. 项目安全准备
  2. 技术架构设计
  3. 第一阶段:开发分支管理
  4. 第二阶段:API 服务器开发
  5. 第三阶段:前端界面开发
  6. 第四阶段:认证系统
  7. 第五阶段:编辑器集成
  8. 第六阶段:版本控制

项目安全准备

1. 创建开发分支保护现有工作

在开始任何开发之前,我们需要保护您现有的工作:

1.1 检查当前状态

bash
git status

含义: 查看当前工作目录的状态,确认是否有未提交的更改

1.2 提交当前所有更改

bash
git add .
git commit -m "保存当前稳定版本 - 准备开发在线编辑功能"

含义: 将当前所有更改提交到 Git,作为一个稳定的保存点

1.3 创建功能开发分支

bash
git checkout -b feature/online-editor

含义: 创建并切换到新分支 feature/online-editor,所有在线编辑功能开发都在这个分支进行

1.4 推送分支到服务器

bash
git push -u origin feature/online-editor

含义: 将新分支推送到服务器,建立远程跟踪关系

2. 服务器端分支配置

2.1 连接到服务器

bash
ssh webnote@43.159.60.248

含义: 连接到您的服务器

2.2 在服务器上创建开发环境

bash
cd /home/webnote/webnote/source/WebNote
git fetch origin
git checkout -b feature/online-editor origin/feature/online-editor

含义: 在服务器上同步并切换到开发分支

2.3 创建开发端口配置

bash
sudo ufw allow 3001

含义: 为 API 服务器开放 3001 端口

技术架构设计

整体架构图

┌─────────────────────────────────────────────────────────────┐
│                    前端用户界面                              │
├─────────────────────────────────────────────────────────────┤
│ VitePress (8080) + 汉堡菜单 + Monaco Editor               │
│                                                             │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │   登录界面   │ │  编辑界面   │ │     草稿箱管理界面      │ │
│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│                   API 中间层 (3001)                        │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │  认证服务   │ │ 文件操作API │ │      版本控制服务       │ │
│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│                   存储和版本控制                            │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Git 仓库    │ │ 用户数据    │ │       草稿存储          │ │
│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

端口分配

  • 8080: Nginx 静态文件服务(生产环境)
  • 3000: VitePress 开发服务器(开发预览)
  • 3001: API 服务器(在线编辑功能)

数据流设计

  1. 用户认证流程: 前端 → API认证 → JWT令牌 → 后续请求携带令牌
  2. 文件编辑流程: 读取文件 → 在线编辑 → 保存草稿/提交 → Git提交 → 自动部署
  3. 冲突检测流程: 获取文件版本 → 编辑时检查版本 → 冲突提示 → 合并或覆盖

第一阶段:开发分支管理

1. 创建项目结构

1.1 在本地创建 API 目录结构

bash
mkdir -p api/{routes,middleware,models,utils,config}
mkdir -p docs/.vitepress/components/editor
mkdir -p docs/.vitepress/stores

含义: 创建完整的项目结构

  • api/: API 服务器代码
  • docs/.vitepress/components/editor/: 编辑器相关组件
  • docs/.vitepress/stores/: 状态管理

1.2 创建配置文件占位符

bash
touch api/package.json
touch api/server.js
touch api/config/auth.js
touch api/config/database.js
touch docs/.vitepress/components/editor/HamburgerMenu.vue
touch docs/.vitepress/components/editor/MarkdownEditor.vue
touch docs/.vitepress/stores/auth.js

含义: 创建主要文件的占位符,为后续开发做准备

1.3 提交初始结构

bash
git add .
git commit -m "添加在线编辑功能的项目结构"

含义: 保存项目结构到 Git

2. 依赖包规划

我们需要安装的主要依赖包:

API 服务器依赖:

  • express: Web 框架
  • express-session: 会话管理
  • bcryptjs: 密码加密
  • jsonwebtoken: JWT 认证
  • multer: 文件上传处理
  • cors: 跨域请求处理
  • simple-git: Git 操作
  • chokidar: 文件变化监听
  • fs-extra: 文件系统操作增强

前端依赖:

  • monaco-editor: 代码编辑器
  • axios: HTTP 客户端
  • pinia: 状态管理(如果使用 Vue 3)

3. 开发环境隔离

3.1 修改开发配置以支持 API 代理

我们需要配置 VitePress 开发服务器代理 API 请求到我们的 Express 服务器。

3.2 创建环境变量文件

bash
touch .env.development
touch .env.production

含义: 创建不同环境的配置文件

4. Git 工作流设置

4.1 配置 Git 忽略文件

bash
echo "
# API 服务器
api/node_modules/
api/logs/
api/.env

# 开发环境
.env.development
.env.production

# 编辑器临时文件
*.tmp
*.draft

# 用户上传文件
uploads/
" >> .gitignore

含义: 更新 .gitignore 文件,排除不需要版本控制的文件

4.2 设置提交模板

bash
git config commit.template .gitmessage
echo "
# 提交类型:
# feat: 新功能
# fix: 修复bug
# docs: 文档更新
# style: 代码格式调整
# refactor: 代码重构
# test: 测试相关
# chore: 构建过程或辅助工具的变动

# 修改内容简述:
# 

# 详细说明(可选):
# 

# 关联的 issue(可选):
# 
" > .gitmessage

含义: 设置 Git 提交消息模板,规范提交格式

现在我们已经完成了第一阶段的准备工作。接下来的阶段将更加详细,包含具体的代码实现。

下一步预告: 第二阶段将开始 API 服务器的开发,包括认证系统、文件操作 API 和版本控制功能。

请确认第一阶段的步骤都已完成,然后我们继续下一阶段的开发

第二阶段:API 服务器开发

1. 初始化 API 服务器项目

1.1 进入 API 目录并初始化 Node.js 项目

bash
cd api
npm init -y

含义: 初始化 Node.js 项目,创建 package.json 文件

1.2 安装基础依赖

bash
npm install express cors dotenv helmet morgan

含义: 安装 Express 框架和基础中间件

  • express: Web 应用框架
  • cors: 处理跨域请求
  • dotenv: 环境变量管理
  • helmet: 安全中间件
  • morgan: HTTP 请求日志

1.3 安装认证相关依赖

bash
npm install bcryptjs jsonwebtoken express-session

含义: 安装用户认证相关的包

  • bcryptjs: 密码哈希加密
  • jsonwebtoken: JWT 令牌生成和验证
  • express-session: 会话管理

1.4 安装文件操作依赖

bash
npm install fs-extra multer simple-git chokidar

含义: 安装文件和 Git 操作相关的包

  • fs-extra: 增强的文件系统操作
  • multer: 文件上传中间件
  • simple-git: Git 操作库
  • chokidar: 文件变化监听

1.5 安装开发依赖

bash
npm install --save-dev nodemon concurrently

含义: 安装开发工具

  • nodemon: 自动重启开发服务器
  • concurrently: 同时运行多个命令

2. 创建基础服务器结构

2.1 创建主服务器文件

bash
cat > server.js << 'EOF'
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
require('dotenv').config();

const app = express();
const PORT = process.env.PORT || 3001;

// 基础中间件
app.use(helmet());
app.use(morgan('combined'));
app.use(cors({
  origin: ['http://43.159.60.248:8080', 'http://43.159.60.248:3000'],
  credentials: true
}));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));

// 健康检查端点
app.get('/api/health', (req, res) => {
  res.json({ 
    status: 'ok', 
    timestamp: new Date().toISOString(),
    uptime: process.uptime()
  });
});

// 错误处理中间件
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ 
    error: 'Internal Server Error',
    message: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong!'
  });
});

// 404 处理
app.use('*', (req, res) => {
  res.status(404).json({ error: 'API endpoint not found' });
});

app.listen(PORT, '0.0.0.0', () => {
  console.log(`API服务器启动在端口 ${PORT}`);
  console.log(`健康检查: http://43.159.60.248:${PORT}/api/health`);
});
EOF

含义: 创建基础的 Express 服务器,包含安全中间件和基本路由

2.2 创建环境配置文件

bash
cat > .env << 'EOF'
NODE_ENV=development
PORT=3001
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
SESSION_SECRET=your-super-secret-session-key-change-this-too
WORK_DIR=/home/webnote/webnote/source/WebNote
GIT_DIR=/home/webnote/webnote/webnote.git
BUILD_DIR=/home/webnote/webnote/build
EOF

含义: 创建环境变量配置文件

2.3 更新 package.json 添加脚本

bash
cat > package.json << 'EOF'
{
  "name": "webnote-api",
  "version": "1.0.0",
  "description": "VitePress在线编辑API服务器",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": ["vitepress", "markdown", "editor", "api"],
  "author": "WebNote",
  "license": "MIT",
  "dependencies": {
    "bcryptjs": "^2.4.3",
    "chokidar": "^3.5.3",
    "cors": "^2.8.5",
    "dotenv": "^16.3.1",
    "express": "^4.18.2",
    "express-session": "^1.17.3",
    "fs-extra": "^11.1.1",
    "helmet": "^7.1.0",
    "jsonwebtoken": "^9.0.2",
    "morgan": "^1.10.0",
    "multer": "^1.4.5-lts.1",
    "simple-git": "^3.19.1"
  },
  "devDependencies": {
    "concurrently": "^8.2.2",
    "nodemon": "^3.0.1"
  }
}
EOF

含义: 配置 package.json,包含所有依赖和启动脚本

3. 创建认证配置

3.1 创建认证配置文件

bash
cat > config/auth.js << 'EOF'
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');

// 用户数据存储(生产环境应使用数据库)
const users = [
  {
    id: 1,
    username: 'admin',
    // 密码: admin123(请在生产环境中更改)
    password: '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi',
    role: 'admin',
    displayName: '管理员'
  },
  {
    id: 2,
    username: 'editor',
    // 密码: editor123(请在生产环境中更改)
    password: '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi',
    role: 'editor',
    displayName: '编辑者'
  }
];

// 验证用户凭据
const validateUser = async (username, password) => {
  const user = users.find(u => u.username === username);
  if (!user) return null;
  
  const isValidPassword = await bcrypt.compare(password, user.password);
  if (!isValidPassword) return null;
  
  // 返回用户信息(不包含密码)
  const { password: _, ...userInfo } = user;
  return userInfo;
};

// 生成 JWT 令牌
const generateToken = (user) => {
  return jwt.sign(
    { 
      id: user.id, 
      username: user.username, 
      role: user.role 
    },
    process.env.JWT_SECRET,
    { expiresIn: '24h' }
  );
};

// 验证 JWT 令牌
const verifyToken = (token) => {
  try {
    return jwt.verify(token, process.env.JWT_SECRET);
  } catch (error) {
    return null;
  }
};

// 生成新用户密码哈希(用于添加新用户)
const hashPassword = async (password) => {
  return await bcrypt.hash(password, 10);
};

module.exports = {
  users,
  validateUser,
  generateToken,
  verifyToken,
  hashPassword
};
EOF

含义: 创建用户认证配置,包含用户数据和认证函数

3.2 创建认证中间件

bash
cat > middleware/auth.js << 'EOF'
const { verifyToken } = require('../config/auth');

// JWT 认证中间件
const authenticateToken = (req, res, next) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN

  if (!token) {
    return res.status(401).json({ error: '访问令牌缺失' });
  }

  const user = verifyToken(token);
  if (!user) {
    return res.status(403).json({ error: '无效或过期的访问令牌' });
  }

  req.user = user;
  next();
};

// 角色权限检查中间件
const requireRole = (roles) => {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: '未认证用户' });
    }

    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: '权限不足' });
    }

    next();
  };
};

// 编辑权限检查(admin 和 editor 都可以编辑)
const requireEditPermission = requireRole(['admin', 'editor']);

module.exports = {
  authenticateToken,
  requireRole,
  requireEditPermission
};
EOF

含义: 创建认证中间件,用于保护 API 路由

4. 创建认证路由

4.1 创建认证 API 路由

bash
cat > routes/auth.js << 'EOF'
const express = require('express');
const { validateUser, generateToken } = require('../config/auth');
const { authenticateToken } = require('../middleware/auth');

const router = express.Router();

// 用户登录
router.post('/login', async (req, res) => {
  try {
    const { username, password } = req.body;

    if (!username || !password) {
      return res.status(400).json({ error: '用户名和密码是必需的' });
    }

    const user = await validateUser(username, password);
    if (!user) {
      return res.status(401).json({ error: '用户名或密码错误' });
    }

    const token = generateToken(user);
    
    res.json({
      success: true,
      token,
      user: {
        id: user.id,
        username: user.username,
        role: user.role,
        displayName: user.displayName
      }
    });
  } catch (error) {
    console.error('登录错误:', error);
    res.status(500).json({ error: '登录过程中发生错误' });
  }
});

// 验证当前用户状态
router.get('/me', authenticateToken, (req, res) => {
  res.json({
    success: true,
    user: req.user
  });
});

// 用户登出(客户端删除令牌即可)
router.post('/logout', authenticateToken, (req, res) => {
  res.json({ success: true, message: '已登出' });
});

module.exports = router;
EOF

含义: 创建用户认证相关的 API 路由

4.2 测试认证服务器

bash
cd ..
npm run dev

含义: 启动开发服务器进行测试

4.3 在另一个终端测试健康检查

bash
curl http://localhost:3001/api/health

含义: 测试 API 服务器是否正常运行

4.4 测试认证功能

bash
# 用户登录
curl -X POST -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"admin123"}' \
  http://localhost:3001/api/auth/login

# 获取当前用户信息
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:3001/api/auth/me

# 用户登出
curl -X POST -H "Authorization: Bearer YOUR_TOKEN" http://localhost:3001/api/auth/logout

含义: 测试用户认证相关的 API 是否正常

5. 集成认证路由到主服务器

5.1 更新主服务器文件

bash
cat > api/server.js << 'EOF'
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
require('dotenv').config();

// 导入路由
const authRoutes = require('./routes/auth');

const app = express();
const PORT = process.env.PORT || 3001;

// 基础中间件
app.use(helmet());
app.use(morgan('combined'));
app.use(cors({
  origin: ['http://43.159.60.248:8080', 'http://43.159.60.248:3000', 'http://localhost:3000'],
  credentials: true
}));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));

// API 路由
app.use('/api/auth', authRoutes);

// 健康检查端点
app.get('/api/health', (req, res) => {
  res.json({ 
    status: 'ok', 
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    version: '1.0.0',
    features: ['authentication', 'file-management', 'git-integration']
  });
});

// API 根路径信息
app.get('/api', (req, res) => {
  res.json({
    name: 'WebNote API',
    version: '1.0.0',
    description: 'VitePress在线编辑API服务器',
    endpoints: {
      health: '/api/health',
      auth: {
        login: 'POST /api/auth/login',
        me: 'GET /api/auth/me',
        logout: 'POST /api/auth/logout'
      }
    }
  });
});

// 错误处理中间件
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ 
    error: 'Internal Server Error',
    message: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong!'
  });
});

// 404 处理
app.use('*', (req, res) => {
  res.status(404).json({ error: 'API endpoint not found' });
});

app.listen(PORT, '0.0.0.0', () => {
  console.log(`🚀 API服务器启动在端口 ${PORT}`);
  console.log(`📋 健康检查: http://43.159.60.248:${PORT}/api/health`);
  console.log(`🔐 认证端点: http://43.159.60.248:${PORT}/api/auth`);
  console.log(`📖 API文档: http://43.159.60.248:${PORT}/api`);
});
EOF

含义: 更新服务器文件,集成认证路由

5.2 测试集成认证后的服务器

bash
cd ..
npm run dev

含义: 重启服务器并测试

bash
# 健康检查
curl http://localhost:3001/api/health

# 登录
curl -X POST -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"admin123"}' \
  http://localhost:3001/api/auth/login

# 获取用户信息
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:3001/api/auth/me

含义: 确认认证功能正常

6. 提交第二阶段代码

6.1 提交 API 服务器基础代码

bash
git add .
git commit -m "feat: 添加API服务器基础架构和用户认证功能

- 创建Express服务器基础结构
- 实现JWT认证系统
- 添加用户登录/登出API
- 配置安全中间件和跨域处理
- 设置开发环境配置"

含义: 提交第二阶段的所有代码到 Git

现在我们已经完成了第二阶段的 API 服务器基础开发。

第二阶段总结:

  • ✅ 创建了完整的 Express API 服务器
  • ✅ 实现了 JWT 用户认证系统
  • ✅ 配置了安全中间件和跨域处理
  • ✅ 设置了开发环境配置

下一步预告: 第三阶段将开发文件操作 API,包括读取、创建、编辑和删除 Markdown 文件的功能。

请测试当前的 API 服务器是否正常运行,然后我们继续下一阶段的开发

第三阶段:文件操作 API 开发

1. 创建文件操作工具类

1.1 创建文件系统工具

bash
cat > api/utils/fileSystem.js << 'EOF'
const fs = require('fs-extra');
const path = require('path');

const DOCS_DIR = path.join(process.env.WORK_DIR, 'docs');

class FileSystemManager {
  constructor() {
    this.docsDir = DOCS_DIR;
  }

  // 获取相对于 docs 目录的安全路径
  getSafePath(relativePath) {
    const fullPath = path.join(this.docsDir, relativePath);
    
    // 确保路径在 docs 目录内(防止路径遍历攻击)
    const normalizedPath = path.normalize(fullPath);
    if (!normalizedPath.startsWith(this.docsDir)) {
      throw new Error('Invalid path: 路径必须在docs目录内');
    }
    
    return normalizedPath;
  }

  // 检查文件是否存在
  async exists(relativePath) {
    try {
      const fullPath = this.getSafePath(relativePath);
      return await fs.pathExists(fullPath);
    } catch (error) {
      return false;
    }
  }

  // 读取文件内容
  async readFile(relativePath) {
    const fullPath = this.getSafePath(relativePath);
    
    if (!await this.exists(relativePath)) {
      throw new Error('文件不存在');
    }
    
    return await fs.readFile(fullPath, 'utf8');
  }

  // 写入文件内容
  async writeFile(relativePath, content) {
    const fullPath = this.getSafePath(relativePath);
    
    // 确保目录存在
    await fs.ensureDir(path.dirname(fullPath));
    
    return await fs.writeFile(fullPath, content, 'utf8');
  }

  // 创建目录
  async createDirectory(relativePath) {
    const fullPath = this.getSafePath(relativePath);
    return await fs.ensureDir(fullPath);
  }

  // 删除文件
  async deleteFile(relativePath) {
    const fullPath = this.getSafePath(relativePath);
    
    if (!await this.exists(relativePath)) {
      throw new Error('文件不存在');
    }
    
    return await fs.remove(fullPath);
  }

  // 列出目录内容
  async listDirectory(relativePath = '') {
    const fullPath = this.getSafePath(relativePath);
    
    if (!await fs.pathExists(fullPath)) {
      throw new Error('目录不存在');
    }
    
    const items = await fs.readdir(fullPath, { withFileTypes: true });
    
    return items.map(item => ({
      name: item.name,
      type: item.isDirectory() ? 'directory' : 'file',
      path: path.join(relativePath, item.name).replace(/\\/g, '/'),
      isMarkdown: item.isFile() && path.extname(item.name) === '.md'
    }));
  }

  // 移动/重命名文件
  async moveFile(oldPath, newPath) {
    const oldFullPath = this.getSafePath(oldPath);
    const newFullPath = this.getSafePath(newPath);
    
    if (!await this.exists(oldPath)) {
      throw new Error('源文件不存在');
    }
    
    // 确保目标目录存在
    await fs.ensureDir(path.dirname(newFullPath));
    
    return await fs.move(oldFullPath, newFullPath);
  }

  // 获取文件统计信息
  async getFileStats(relativePath) {
    const fullPath = this.getSafePath(relativePath);
    
    if (!await this.exists(relativePath)) {
      throw new Error('文件不存在');
    }
    
    const stats = await fs.stat(fullPath);
    return {
      size: stats.size,
      created: stats.birthtime,
      modified: stats.mtime,
      isDirectory: stats.isDirectory(),
      isFile: stats.isFile()
    };
  }
}

module.exports = new FileSystemManager();
EOF

含义: 创建文件系统管理工具,提供安全的文件操作功能

1.2 创建 Git 操作工具

bash
cat > api/utils/gitManager.js << 'EOF'
const simpleGit = require('simple-git');
const path = require('path');

class GitManager {
  constructor() {
    this.workDir = process.env.WORK_DIR;
    this.git = simpleGit(this.workDir);
  }

  // 获取当前状态
  async getStatus() {
    try {
      return await this.git.status();
    } catch (error) {
      console.error('Git status error:', error);
      throw new Error('无法获取Git状态');
    }
  }

  // 添加文件到暂存区
  async addFile(filePath) {
    try {
      return await this.git.add(filePath);
    } catch (error) {
      console.error('Git add error:', error);
      throw new Error('无法添加文件到Git');
    }
  }

  // 提交更改
  async commit(message, author = null) {
    try {
      const options = {};
      if (author) {
        options['--author'] = `${author.name} <${author.email}>`;
      }
      
      return await this.git.commit(message, undefined, options);
    } catch (error) {
      console.error('Git commit error:', error);
      throw new Error('Git提交失败');
    }
  }

  // 获取文件的最后修改信息
  async getLastModified(filePath) {
    try {
      const log = await this.git.log(['-1', '--', filePath]);
      if (log.latest) {
        return {
          hash: log.latest.hash,
          date: log.latest.date,
          message: log.latest.message,
          author: {
            name: log.latest.author_name,
            email: log.latest.author_email
          }
        };
      }
      return null;
    } catch (error) {
      console.error('Git log error:', error);
      return null;
    }
  }

  // 检查文件是否有未提交的更改
  async hasUncommittedChanges(filePath) {
    try {
      const status = await this.getStatus();
      const relativePath = path.relative(this.workDir, filePath);
      
      return status.files.some(file => 
        file.path === relativePath || file.path === filePath
      );
    } catch (error) {
      console.error('Git changes check error:', error);
      return false;
    }
  }

  // 获取文件的版本历史
  async getFileHistory(filePath, limit = 10) {
    try {
      const log = await this.git.log([`-${limit}`, '--', filePath]);
      return log.all.map(commit => ({
        hash: commit.hash,
        date: commit.date,
        message: commit.message,
        author: {
          name: commit.author_name,
          email: commit.author_email
        }
      }));
    } catch (error) {
      console.error('Git history error:', error);
      return [];
    }
  }

  // 创建分支
  async createBranch(branchName) {
    try {
      return await this.git.checkoutLocalBranch(branchName);
    } catch (error) {
      console.error('Git branch creation error:', error);
      throw new Error('无法创建分支');
    }
  }

  // 切换分支
  async switchBranch(branchName) {
    try {
      return await this.git.checkout(branchName);
    } catch (error) {
      console.error('Git checkout error:', error);
      throw new Error('无法切换分支');
    }
  }
}

module.exports = new GitManager();
EOF

含义: 创建 Git 操作管理工具,用于版本控制功能

2. 创建文件操作 API 路由

2.1 创建文件管理 API

bash
cat > api/routes/files.js << 'EOF'
const express = require('express');
const fileSystem = require('../utils/fileSystem');
const gitManager = require('../utils/gitManager');
const { authenticateToken, requireEditPermission } = require('../middleware/auth');
const autoPush = require('../utils/autoPush');

const router = express.Router();

// 获取目录结构
router.get('/tree', authenticateToken, async (req, res) => {
  try {
    const { path: requestPath = '' } = req.query;
    const items = await fileSystem.listDirectory(requestPath);
    
    res.json({
      success: true,
      path: requestPath,
      items
    });
  } catch (error) {
    console.error('List directory error:', error);
    res.status(500).json({ error: '无法读取目录' });
  }
});

// 读取文件内容
router.get('/content', authenticateToken, async (req, res) => {
  try {
    const { path: filePath } = req.query;
    
    if (!filePath) {
      return res.status(400).json({ error: '文件路径是必需的' });
    }
    
    const content = await fileSystem.readFile(filePath);
    const stats = await fileSystem.getFileStats(filePath);
    const lastModified = await gitManager.getLastModified(filePath);
    const hasChanges = await gitManager.hasUncommittedChanges(filePath);
    
    res.json({
      success: true,
      path: filePath,
      content,
      stats,
      lastModified,
      hasUncommittedChanges: hasChanges
    });
  } catch (error) {
    console.error('Read file error:', error);
    res.status(500).json({ error: '无法读取文件内容' });
  }
});

// 保存文件内容
router.post('/save', authenticateToken, requireEditPermission, async (req, res) => {
  try {
    const { path: filePath, content, commitMessage, isDraft = false } = req.body;
    
    if (!filePath || content === undefined) {
      return res.status(400).json({ error: '文件路径和内容是必需的' });
    }
    
    // 保存文件
    await fileSystem.writeFile(filePath, content);
    
    if (!isDraft) {
      // 不是草稿,直接提交到Git
      await gitManager.addFile(filePath);
      const message = commitMessage || `更新文件: ${filePath}`;
      const author = {
        name: req.user.displayName || req.user.username,
        email: `${req.user.username}@webnote.local`
      };
      
      await gitManager.commit(message, author);
      await autoPush(); // 新增:自动推送到远程仓库
    }
    
    res.json({
      success: true,
      message: isDraft ? '草稿已保存' : '文件已保存并提交',
      path: filePath
    });
  } catch (error) {
    console.error('Save file error:', error);
    res.status(500).json({ error: '保存文件失败' });
  }
});

// 创建新文件
router.post('/create', authenticateToken, requireEditPermission, async (req, res) => {
  try {
    const { path: filePath, content = '', type = 'file' } = req.body;
    
    if (!filePath) {
      return res.status(400).json({ error: '文件路径是必需的' });
    }
    
    if (await fileSystem.exists(filePath)) {
      return res.status(409).json({ error: '文件已存在' });
    }
    
    if (type === 'directory') {
      await fileSystem.createDirectory(filePath);
    } else {
      await fileSystem.writeFile(filePath, content);
    }
    
    // 添加到Git并提交
    if (type === 'file') {
      await gitManager.addFile(filePath);
      const message = `创建新文件: ${filePath}`;
      const author = {
        name: req.user.displayName || req.user.username,
        email: `${req.user.username}@webnote.local`
      };
      
      await gitManager.commit(message, author);
    }
    
    res.json({
      success: true,
      message: type === 'directory' ? '目录已创建' : '文件已创建',
      path: filePath,
      type
    });
  } catch (error) {
    console.error('Create file error:', error);
    res.status(500).json({ error: '创建文件失败' });
  }
});

// 删除文件
router.delete('/delete', authenticateToken, requireEditPermission, async (req, res) => {
  try {
    const { path: filePath } = req.body;
    
    if (!filePath) {
      return res.status(400).json({ error: '文件路径是必需的' });
    }
    
    await fileSystem.deleteFile(filePath);
    
    // 提交删除操作到Git
    await gitManager.addFile(filePath); // 这会暂存删除操作
    const message = `删除文件: ${filePath}`;
    const author = {
      name: req.user.displayName || req.user.username,
      email: `${req.user.username}@webnote.local`
    };
    
    await gitManager.commit(message, author);
    
    res.json({
      success: true,
      message: '文件已删除',
      path: filePath
    });
  } catch (error) {
    console.error('Delete file error:', error);
    res.status(500).json({ error: '删除文件失败' });
  }
});

// 重命名/移动文件
router.post('/move', authenticateToken, requireEditPermission, async (req, res) => {
  try {
    const { oldPath, newPath } = req.body;
    
    if (!oldPath || !newPath) {
      return res.status(400).json({ error: '原路径和新路径都是必需的' });
    }
    
    if (await fileSystem.exists(newPath)) {
      return res.status(409).json({ error: '目标文件已存在' });
    }
    
    await fileSystem.moveFile(oldPath, newPath);
    
    // 提交移动操作到Git
    await gitManager.addFile(oldPath);
    await gitManager.addFile(newPath);
    const message = `移动文件: ${oldPath} -> ${newPath}`;
    const author = {
      name: req.user.displayName || req.user.username,
      email: `${req.user.username}@webnote.local`
    };
    
    await gitManager.commit(message, author);
    
    res.json({
      success: true,
      message: '文件已移动',
      oldPath,
      newPath
    });
  } catch (error) {
    console.error('Move file error:', error);
    res.status(500).json({ error: '移动文件失败' });
  }
});

// 获取文件历史记录
router.get('/history', authenticateToken, async (req, res) => {
  try {
    const { path: filePath, limit = 10 } = req.query;
    
    if (!filePath) {
      return res.status(400).json({ error: '文件路径是必需的' });
    }
    
    const history = await gitManager.getFileHistory(filePath, parseInt(limit));
    
    res.json({
      success: true,
      path: filePath,
      history
    });
  } catch (error) {
    console.error('Get history error:', error);
    res.status(500).json({ error: '获取历史记录失败' });
  }
});

module.exports = router;
EOF

含义: 创建完整的文件操作 API,包括读取、写入、创建、删除等功能

3. 创建草稿管理功能

3.1 创建草稿存储工具

bash
cat > api/utils/draftManager.js << 'EOF'
const fs = require('fs-extra');
const path = require('path');

class DraftManager {
  constructor() {
    this.draftDir = path.join(process.env.WORK_DIR, '.drafts');
    this.ensureDraftDir();
  }

  async ensureDraftDir() {
    await fs.ensureDir(this.draftDir);
  }

  // 生成草稿文件名
  getDraftFileName(filePath, userId) {
    const relativePath = filePath.replace(/[\/\\]/g, '_');
    return `${userId}_${relativePath}_${Date.now()}.draft`;
  }

  // 保存草稿
  async saveDraft(filePath, content, userId, metadata = {}) {
    const draftFileName = this.getDraftFileName(filePath, userId);
    const draftPath = path.join(this.draftDir, draftFileName);
    
    const draftData = {
      originalPath: filePath,
      content,
      userId,
      timestamp: new Date().toISOString(),
      metadata
    };
    
    await fs.writeJson(draftPath, draftData, { spaces: 2 });
    
    return {
      draftId: draftFileName,
      path: draftPath,
      timestamp: draftData.timestamp
    };
  }

  // 获取用户的草稿列表
  async getUserDrafts(userId) {
    const files = await fs.readdir(this.draftDir);
    const userDrafts = files.filter(file => 
      file.startsWith(`${userId}_`) && file.endsWith('.draft')
    );
    
    const drafts = [];
    for (const file of userDrafts) {
      try {
        const draftPath = path.join(this.draftDir, file);
        const draftData = await fs.readJson(draftPath);
        drafts.push({
          draftId: file,
          originalPath: draftData.originalPath,
          timestamp: draftData.timestamp,
          metadata: draftData.metadata
        });
      } catch (error) {
        console.error(`Error reading draft ${file}:`, error);
      }
    }
    
    return drafts.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
  }

  // 获取草稿内容
  async getDraft(draftId) {
    const draftPath = path.join(this.draftDir, draftId);
    
    if (!await fs.pathExists(draftPath)) {
      throw new Error('草稿不存在');
    }
    
    return await fs.readJson(draftPath);
  }

  // 删除草稿
  async deleteDraft(draftId) {
    const draftPath = path.join(this.draftDir, draftId);
    
    if (!await fs.pathExists(draftPath)) {
      throw new Error('草稿不存在');
    }
    
    await fs.remove(draftPath);
  }

  // 清理过期草稿(7天前的)
  async cleanupOldDrafts(daysOld = 7) {
    const files = await fs.readdir(this.draftDir);
    const cutoffTime = new Date(Date.now() - daysOld * 24 * 60 * 60 * 1000);
    
    for (const file of files) {
      if (!file.endsWith('.draft')) continue;
      
      try {
        const draftPath = path.join(this.draftDir, file);
        const draftData = await fs.readJson(draftPath);
        const draftTime = new Date(draftData.timestamp);
        
        if (draftTime < cutoffTime) {
          await fs.remove(draftPath);
          console.log(`Cleaned up old draft: ${file}`);
        }
      } catch (error) {
        console.error(`Error cleaning draft ${file}:`, error);
      }
    }
  }
}

module.exports = new DraftManager();
EOF

含义: 创建草稿管理工具,支持保存、获取和管理用户草稿

3.2 创建草稿 API 路由

bash
cat > api/routes/drafts.js << 'EOF'
const express = require('express');
const draftManager = require('../utils/draftManager');
const { authenticateToken } = require('../middleware/auth');

const router = express.Router();

// 保存草稿
router.post('/save', authenticateToken, async (req, res) => {
  try {
    const { path: filePath, content, metadata = {} } = req.body;
    
    if (!filePath || content === undefined) {
      return res.status(400).json({ error: '文件路径和内容是必需的' });
    }
    
    const draft = await draftManager.saveDraft(
      filePath, 
      content, 
      req.user.id, 
      {
        ...metadata,
        author: req.user.displayName || req.user.username
      }
    );
    
    res.json({
      success: true,
      message: '草稿已保存',
      draft
    });
  } catch (error) {
    console.error('Save draft error:', error);
    res.status(500).json({ error: '保存草稿失败' });
  }
});

// 获取用户草稿列表
router.get('/list', authenticateToken, async (req, res) => {
  try {
    const drafts = await draftManager.getUserDrafts(req.user.id);
    
    res.json({
      success: true,
      drafts
    });
  } catch (error) {
    console.error('Get drafts error:', error);
    res.status(500).json({ error: '获取草稿列表失败' });
  }
});

// 获取特定草稿内容
router.get('/:draftId', authenticateToken, async (req, res) => {
  try {
    const { draftId } = req.params;
    const draft = await draftManager.getDraft(draftId);
    
    // 验证草稿属于当前用户
    if (draft.userId !== req.user.id) {
      return res.status(403).json({ error: '无权访问此草稿' });
    }
    
    res.json({
      success: true,
      draft
    });
  } catch (error) {
    console.error('Get draft error:', error);
    res.status(500).json({ error: '获取草稿失败' });
  }
});

// 删除草稿
router.delete('/:draftId', authenticateToken, async (req, res) => {
  try {
    const { draftId } = req.params;
    
    // 先验证草稿属于当前用户
    const draft = await draftManager.getDraft(draftId);
    if (draft.userId !== req.user.id) {
      return res.status(403).json({ error: '无权删除此草稿' });
    }
    
    await draftManager.deleteDraft(draftId);
    
    res.json({
      success: true,
      message: '草稿已删除'
    });
  } catch (error) {
    console.error('Delete draft error:', error);
    res.status(500).json({ error: '删除草稿失败' });
  }
});

module.exports = router;
EOF

含义: 创建草稿相关的 API 路由

4. 更新主服务器整合所有路由

4.1 更新服务器主文件

bash
cat > api/server.js << 'EOF'
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const http = require('http');
const CollaborationManager = require('./utils/collaboration');
require('dotenv').config();

// 导入路由
const authRoutes = require('./routes/auth');
const fileRoutes = require('./routes/files');
const draftRoutes = require('./routes/drafts');

const app = express();
const server = http.createServer(app);
const PORT = process.env.PORT || 3001;

// 初始化协作管理器
const collaboration = new CollaborationManager(server);

// 基础中间件
app.use(helmet());
app.use(morgan('combined'));
app.use(cors({
  origin: [
    'http://43.159.60.248:8080', 
    'http://43.159.60.248:3000', 
    'http://localhost:3000',
    'http://localhost:8080'
  ],
  credentials: true
}));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));

// 将协作管理器添加到请求对象
app.use((req, res, next) => {
  req.collaboration = collaboration;
  next();
});

// API 路由
app.use('/api/auth', authRoutes);
app.use('/api/files', fileRoutes);
app.use('/api/drafts', draftRoutes);

// 获取文件编辑者信息
app.get('/api/collaboration/file-editors/:filePath', (req, res) => {
  const filePath = decodeURIComponent(req.params.filePath);
  const editors = collaboration.getFileEditors(filePath);
  res.json({ success: true, editors });
});

// 健康检查端点
app.get('/api/health', (req, res) => {
  res.json({ 
    status: 'ok', 
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    version: '1.0.0',
    features: ['authentication', 'file-management', 'git-integration', 'draft-management', 'real-time-collaboration']
  });
});

// API 根路径信息
app.get('/api', (req, res) => {
  res.json({
    name: 'WebNote API',
    version: '1.0.0',
    description: 'VitePress在线编辑API服务器',
    endpoints: {
      health: '/api/health',
      auth: {
        login: 'POST /api/auth/login',
        me: 'GET /api/auth/me',
        logout: 'POST /api/auth/logout'
      },
      files: {
        tree: 'GET /api/files/tree',
        content: 'GET /api/files/content',
        save: 'POST /api/files/save',
        create: 'POST /api/files/create',
        delete: 'DELETE /api/files/delete',
        move: 'POST /api/files/move',
        history: 'GET /api/files/history'
      },
      drafts: {
        save: 'POST /api/drafts/save',
        list: 'GET /api/drafts/list',
        get: 'GET /api/drafts/:draftId',
        delete: 'DELETE /api/drafts/:draftId'
      },
      collaboration: {
        fileEditors: 'GET /api/collaboration/file-editors/:filePath'
      }
    }
  });
});

// 错误处理中间件
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ 
    error: 'Internal Server Error',
    message: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong!'
  });
});

// 404 处理
app.use('*', (req, res) => {
  res.status(404).json({ error: 'API endpoint not found' });
});

server.listen(PORT, '0.0.0.0', () => {
  console.log(`🚀 API服务器启动在端口 ${PORT}`);
  console.log(`📋 健康检查: http://43.159.60.248:${PORT}/api/health`);
  console.log(`🔐 认证端点: http://43.159.60.248:${PORT}/api/auth`);
  console.log(`📁 文件管理: http://43.159.60.248:${PORT}/api/files`);
  console.log(`📝 草稿管理: http://43.159.60.248:${PORT}/api/drafts`);
  console.log(`👥 实时协作: WebSocket 已启用`);
  console.log(`📖 API文档: http://43.159.60.248:${PORT}/api`);
});
EOF

含义: 更新主服务器,集成 WebSocket 实时协作功能

4.2 测试集成协作后的服务器

bash
cd ..
npm run dev

含义: 重启服务器并测试

bash
# 健康检查
curl http://localhost:3001/api/health

# 登录
curl -X POST -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"admin123"}' \
  http://localhost:3001/api/auth/login

# 获取用户信息
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:3001/api/auth/me

含义: 确认认证功能正常

5. 提交第三阶段代码

5.1 提交所有文件操作功能

bash
git add .
git commit -m "feat: 实现完整的文件操作和草稿管理API

- 添加文件系统管理工具类
- 实现Git版本控制操作
- 创建文件CRUD操作API
- 添加草稿保存和管理功能
- 支持文件历史记录查询
- 集成所有API路由到主服务器"

含义: 提交第三阶段的所有代码

现在我们已经完成了第三阶段的文件操作 API 开发。

第三阶段总结:

  • ✅ 创建了安全的文件系统管理工具
  • ✅ 实现了 Git 版本控制集成
  • ✅ 开发了完整的文件 CRUD API
  • ✅ 添加了草稿管理功能
  • ✅ 支持文件历史记录查询

下一步预告: 第四阶段将开发前端界面,包括汉堡菜单、登录界面和 Monaco Editor 集成。

请测试当前的 API 功能是否正常工作,然后我们继续第四阶段的开发

第四阶段:前端界面开发

1. 安装前端依赖

1.1 安装 Monaco Editor 和相关依赖

bash
npm install monaco-editor @monaco-editor/loader axios

含义: 安装 Monaco Editor(VS Code 的编辑器组件)和 HTTP 请求库

1.2 安装 Vue 3 状态管理(如果需要)

bash
npm install pinia

含义: 安装 Pinia 状态管理库(Vue 3 推荐)

2. 创建认证状态管理

2.1 创建认证 store

bash
cat > docs/.vitepress/stores/auth.js << 'EOF'
import { ref, computed } from 'vue'
import axios from 'axios'

// API 基础配置
const API_BASE = 'http://43.159.60.248:3001'

// 创建 axios 实例
const api = axios.create({
  baseURL: API_BASE,
  timeout: 10000,
  withCredentials: true
})

// 状态
const user = ref(null)
const token = ref(localStorage.getItem('webnote_token'))
const isLoading = ref(false)

// 计算属性
const isAuthenticated = computed(() => !!token.value && !!user.value)
const isAdmin = computed(() => user.value?.role === 'admin')
const canEdit = computed(() => ['admin', 'editor'].includes(user.value?.role))

// 设置请求拦截器
api.interceptors.request.use((config) => {
  if (token.value) {
    config.headers.Authorization = `Bearer ${token.value}`
  }
  return config
})

// 设置响应拦截器
api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      // 令牌过期或无效,清除认证状态
      logout()
    }
    return Promise.reject(error)
  }
)

// 认证方法
const login = async (username, password) => {
  try {
    isLoading.value = true
    const response = await api.post('/api/auth/login', {
      username,
      password
    })
    
    if (response.data.success) {
      token.value = response.data.token
      user.value = response.data.user
      
      // 保存到本地存储
      localStorage.setItem('webnote_token', token.value)
      localStorage.setItem('webnote_user', JSON.stringify(user.value))
      
      return { success: true }
    }
  } catch (error) {
    console.error('Login error:', error)
    return { 
      success: false, 
      message: error.response?.data?.error || '登录失败' 
    }
  } finally {
    isLoading.value = false
  }
}

const logout = async () => {
  try {
    if (token.value) {
      await api.post('/api/auth/logout')
    }
  } catch (error) {
    console.error('Logout error:', error)
  } finally {
    // 清除状态和本地存储
    token.value = null
    user.value = null
    localStorage.removeItem('webnote_token')
    localStorage.removeItem('webnote_user')
  }
}

const checkAuth = async () => {
  if (!token.value) return false
  
  try {
    const response = await api.get('/api/auth/me')
    if (response.data.success) {
      user.value = response.data.user
      return true
    }
  } catch (error) {
    console.error('Auth check error:', error)
    logout()
  }
  return false
}

// 初始化:从本地存储恢复用户信息
const initAuth = () => {
  const savedUser = localStorage.getItem('webnote_user')
  if (savedUser && token.value) {
    try {
      user.value = JSON.parse(savedUser)
      // 验证令牌是否仍然有效
      checkAuth()
    } catch (error) {
      console.error('Error parsing saved user:', error)
      logout()
    }
  }
}

export {
  api,
  user,
  token,
  isLoading,
  isAuthenticated,
  isAdmin,
  canEdit,
  login,
  logout,
  checkAuth,
  initAuth
}
EOF

含义: 创建认证状态管理,处理用户登录、登出和权限验证

3. 创建汉堡菜单组件

3.1 创建汉堡菜单主组件

bash
cat > docs/.vitepress/components/editor/HamburgerMenu.vue << 'EOF'
<template>
  <div class="hamburger-menu">
    <!-- 汉堡菜单按钮 -->
    <button 
      class="menu-toggle"
      @click="toggleMenu"
      :class="{ active: isMenuOpen }"
      v-if="showMenuButton"
    >
      <span></span>
      <span></span>
      <span></span>
    </button>

    <!-- 侧边栏菜单 -->
    <div 
      class="sidebar"
      :class="{ open: isMenuOpen }"
      v-if="showMenuButton"
    >
      <div class="sidebar-header">
        <h3>WebNote 编辑器</h3>
        <button class="close-btn" @click="closeMenu">×</button>
      </div>

      <div class="sidebar-content">
        <!-- 用户信息 -->
        <div class="user-info" v-if="isAuthenticated">
          <div class="user-avatar">
            {{ user?.displayName?.[0] || user?.username?.[0] || 'U' }}
          </div>
          <div class="user-details">
            <div class="user-name">{{ user?.displayName || user?.username }}</div>
            <div class="user-role">{{ user?.role }}</div>
          </div>
        </div>

        <!-- 登录表单 -->
        <div class="login-form" v-if="!isAuthenticated">
          <h4>登录</h4>
          <form @submit.prevent="handleLogin">
            <div class="form-group">
              <input
                v-model="loginForm.username"
                type="text"
                placeholder="用户名"
                required
              />
            </div>
            <div class="form-group">
              <input
                v-model="loginForm.password"
                type="password"
                placeholder="密码"
                required
              />
            </div>
            <button 
              type="submit" 
              class="login-btn"
              :disabled="isLoading"
            >
              {{ isLoading ? '登录中...' : '登录' }}
            </button>
          </form>
          <div class="login-error" v-if="loginError">
            {{ loginError }}
          </div>
        </div>

        <!-- 编辑功能菜单 -->
        <div class="edit-menu" v-if="isAuthenticated && canEdit">
          <h4>编辑功能</h4>
          <div class="menu-items">
            <button 
              class="menu-item"
              @click="openFileTree"
            >
              📁 文件管理
            </button>
            <button 
              class="menu-item"
              @click="createNewFile"
            >
              📄 新建文件
            </button>
            <button 
              class="menu-item"
              @click="openDrafts"
            >
              📝 草稿箱
            </button>
            <button 
              class="menu-item"
              @click="openCurrentFile"
              v-if="currentFile"
            >
              ✏️ 编辑当前文件
            </button>
          </div>
        </div>

        <!-- 系统功能 -->
        <div class="system-menu">
          <h4>系统</h4>
          <div class="menu-items">
            <button 
              class="menu-item"
              @click="refreshPage"
            >
              🔄 刷新页面
            </button>
            <button 
              class="menu-item"
              @click="handleLogout"
              v-if="isAuthenticated"
            >
              🚪 登出
            </button>
          </div>
        </div>
      </div>
    </div>

    <!-- 遮罩层 -->
    <div 
      class="overlay"
      v-if="isMenuOpen"
      @click="closeMenu"
    ></div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'
import { 
  user, 
  isAuthenticated, 
  canEdit, 
  isLoading, 
  login, 
  logout, 
  initAuth 
} from '../../stores/auth.js'

// 响应式数据
const isMenuOpen = ref(false)
const loginForm = ref({
  username: '',
  password: ''
})
const loginError = ref('')

// 计算属性
const showMenuButton = computed(() => {
  // 检查是否在文档页面(可以根据路径判断)
  return typeof window !== 'undefined' && window.location.pathname.startsWith('/docs')
})

const currentFile = computed(() => {
  // 从当前路径获取文件信息
  if (typeof window === 'undefined') return null
  const path = window.location.pathname
  if (path.endsWith('.html')) {
    return path.replace('.html', '.md').replace('/docs/', '')
  }
  return null
})

// 方法
const toggleMenu = () => {
  isMenuOpen.value = !isMenuOpen.value
}

const closeMenu = () => {
  isMenuOpen.value = false
}

const handleLogin = async () => {
  loginError.value = ''
  const result = await login(loginForm.value.username, loginForm.value.password)
  
  if (result.success) {
    loginForm.value.username = ''
    loginForm.value.password = ''
    closeMenu()
  } else {
    loginError.value = result.message
  }
}

const handleLogout = async () => {
  await logout()
  closeMenu()
}

const openFileTree = () => {
  // 打开文件树组件
  window.dispatchEvent(new CustomEvent('open-file-tree'))
  closeMenu()
}

const createNewFile = () => {
  // 打开新建文件对话框
  window.dispatchEvent(new CustomEvent('create-new-file'))
  closeMenu()
}

const openDrafts = () => {
  // 打开草稿箱
  window.dispatchEvent(new CustomEvent('open-drafts'))
  closeMenu()
}

const openCurrentFile = () => {
  if (currentFile.value) {
    // 打开当前文件编辑器
    window.dispatchEvent(new CustomEvent('edit-file', { 
      detail: { filePath: currentFile.value } 
    }))
    closeMenu()
  }
}

const refreshPage = () => {
  window.location.reload()
}

// 生命周期
onMounted(() => {
  initAuth()
})
</script>

<style scoped>
.hamburger-menu {
  position: fixed;
  top: 20px;
  right: 20px;
  z-index: 9999;
}

/* 汉堡菜单按钮 */
.menu-toggle {
  display: flex;
  flex-direction: column;
  justify-content: space-around;
  width: 40px;
  height: 40px;
  background: #2d3748;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  padding: 8px;
  transition: all 0.3s ease;
  box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}

.menu-toggle:hover {
  background: #4a5568;
  transform: scale(1.05);
}

.menu-toggle span {
  width: 100%;
  height: 3px;
  background: white;
  border-radius: 2px;
  transition: all 0.3s ease;
}

.menu-toggle.active span:nth-child(1) {
  transform: rotate(45deg) translate(8px, 8px);
}

.menu-toggle.active span:nth-child(2) {
  opacity: 0;
}

.menu-toggle.active span:nth-child(3) {
  transform: rotate(-45deg) translate(8px, -8px);
}

/* 侧边栏 */
.sidebar {
  position: fixed;
  top: 0;
  right: -400px;
  width: 350px;
  height: 100vh;
  background: white;
  box-shadow: -4px 0 20px rgba(0,0,0,0.1);
  transition: right 0.3s ease;
  overflow-y: auto;
  border-left: 1px solid #e2e8f0;
}

.sidebar.open {
  right: 0;
}

.sidebar-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20px;
  border-bottom: 1px solid #e2e8f0;
  background: #f7fafc;
}

.sidebar-header h3 {
  margin: 0;
  color: #2d3748;
  font-size: 18px;
}

.close-btn {
  background: none;
  border: none;
  font-size: 24px;
  cursor: pointer;
  color: #718096;
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
}

.close-btn:hover {
  background: #e2e8f0;
}

/* 侧边栏内容 */
.sidebar-content {
  padding: 20px;
}

/* 用户信息 */
.user-info {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 16px;
  background: #edf2f7;
  border-radius: 8px;
  margin-bottom: 24px;
}

.user-avatar {
  width: 40px;
  height: 40px;
  background: #4299e1;
  color: white;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: bold;
  text-transform: uppercase;
}

.user-details {
  flex: 1;
}

.user-name {
  font-weight: 600;
  color: #2d3748;
}

.user-role {
  font-size: 12px;
  color: #718096;
  text-transform: capitalize;
}

/* 登录表单 */
.login-form {
  margin-bottom: 24px;
}

.login-form h4 {
  margin: 0 0 16px 0;
  color: #2d3748;
}

.form-group {
  margin-bottom: 12px;
}

.form-group input {
  width: 100%;
  padding: 12px;
  border: 1px solid #e2e8f0;
  border-radius: 6px;
  font-size: 14px;
  transition: border-color 0.2s;
}

.form-group input:focus {
  outline: none;
  border-color: #4299e1;
  box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.1);
}

.login-btn {
  width: 100%;
  padding: 12px;
  background: #4299e1;
  color: white;
  border: none;
  border-radius: 6px;
  font-size: 14px;
  cursor: pointer;
  transition: background 0.2s;
}

.login-btn:hover:not(:disabled) {
  background: #3182ce;
}

.login-btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.login-error {
  margin-top: 12px;
  padding: 8px;
  background: #fed7d7;
  color: #c53030;
  border-radius: 4px;
  font-size: 13px;
}

/* 菜单部分 */
.edit-menu, .system-menu {
  margin-bottom: 24px;
}

.edit-menu h4, .system-menu h4 {
  margin: 0 0 12px 0;
  color: #2d3748;
  font-size: 14px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.menu-items {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.menu-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px;
  background: none;
  border: 1px solid #e2e8f0;
  border-radius: 6px;
  cursor: pointer;
  transition: all 0.2s;
  text-align: left;
  font-size: 14px;
  color: #4a5568;
}

.menu-item:hover {
  background: #f7fafc;
  border-color: #cbd5e0;
  transform: translateY(-1px);
}

/* 遮罩层 */
.overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0,0,0,0.5);
  z-index: 9998;
}

/* 响应式设计 */
@media (max-width: 768px) {
  .sidebar {
    width: 100vw;
    right: -100vw;
  }
  
  .hamburger-menu {
    top: 10px;
    right: 10px;
  }
}
</style>
EOF

含义: 创建汉堡菜单组件,包含用户认证、文件管理和编辑功能

4. 创建文件树浏览组件

4.1 创建文件树组件

bash
cat > docs/.vitepress/components/editor/FileTree.vue << 'EOF'
<template>
  <div class="file-tree-modal" v-if="isVisible" @click.self="close">
    <div class="file-tree-container">
      <div class="file-tree-header">
        <h3>文件管理</h3>
        <button class="close-btn" @click="close">×</button>
      </div>
      
      <div class="file-tree-toolbar">
        <div class="current-path">
          <span class="path-label">当前目录:</span>
          <span class="path-value">{{ currentPath || '/' }}</span>
        </div>
        <div class="toolbar-actions">
          <button 
            class="toolbar-btn"
            @click="goBack"
            :disabled="currentPath === ''"
          >
            ← 上级
          </button>
          <button 
            class="toolbar-btn"
            @click="refresh"
          >
            🔄 刷新
          </button>
        </div>
      </div>

      <div class="file-tree-content">
        <div class="loading" v-if="isLoading">
          加载中...
        </div>
        
        <div class="error" v-else-if="error">
          {{ error }}
        </div>
        
        <div class="file-list" v-else>
          <div 
            class="file-item"
            v-for="item in items"
            :key="item.path"
            @click="handleItemClick(item)"
            @dblclick="handleItemDoubleClick(item)"
          >
            <div class="file-icon">
              {{ item.type === 'directory' ? '📁' : (item.isMarkdown ? '📄' : '📋') }}
            </div>
            <div class="file-name">{{ item.name }}</div>
            <div class="file-actions" v-if="canEdit">
              <button 
                class="action-btn"
                @click.stop="editItem(item)"
                v-if="item.isMarkdown"
                title="编辑文件"
              >
                ✏️
              </button>
              <button 
                class="action-btn"
                @click.stop="deleteItem(item)"
                title="删除"
              >
                🗑️
              </button>
            </div>
          </div>
        </div>
      </div>

      <div class="file-tree-footer" v-if="canEdit">
        <button class="create-btn" @click="showCreateDialog = true">
          + 新建
        </button>
      </div>
    </div>

    <!-- 新建文件/目录对话框 -->
    <div class="create-dialog" v-if="showCreateDialog" @click.self="closeCreateDialog">
      <div class="dialog-content">
        <h4>创建新项目</h4>
        <div class="form-group">
          <label>类型:</label>
          <select v-model="createForm.type">
            <option value="file">文件</option>
            <option value="directory">目录</option>
          </select>
        </div>
        <div class="form-group">
          <label>名称:</label>
          <input 
            v-model="createForm.name"
            type="text"
            :placeholder="createForm.type === 'file' ? '例如: new-page.md' : '例如: new-folder'"
            @keyup.enter="createItem"
          />
        </div>
        <div class="dialog-actions">
          <button @click="closeCreateDialog">取消</button>
          <button @click="createItem" :disabled="!createForm.name">创建</button>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { api, canEdit } from '../../stores/auth.js'

// 响应式数据
const isVisible = ref(false)
const isLoading = ref(false)
const error = ref('')
const currentPath = ref('')
const items = ref([])
const showCreateDialog = ref(false)
const createForm = ref({
  type: 'file',
  name: ''
})

// 方法
const show = () => {
  isVisible.value = true
  loadDirectory()
}

const close = () => {
  isVisible.value = false
  currentPath.value = ''
  items.value = []
}

const loadDirectory = async (path = '') => {
  try {
    isLoading.value = true
    error.value = ''
    
    const response = await api.get('/api/files/tree', {
      params: { path }
    })
    
    if (response.data.success) {
      items.value = response.data.items
      currentPath.value = path
    }
  } catch (err) {
    error.value = err.response?.data?.error || '加载目录失败'
  } finally {
    isLoading.value = false
  }
}

const goBack = () => {
  const pathParts = currentPath.value.split('/').filter(Boolean)
  pathParts.pop()
  const parentPath = pathParts.join('/')
  loadDirectory(parentPath)
}

const refresh = () => {
  loadDirectory(currentPath.value)
}

const handleItemClick = (item) => {
  // 单击选中
  console.log('Selected:', item.name)
}

const handleItemDoubleClick = (item) => {
  if (item.type === 'directory') {
    loadDirectory(item.path)
  } else if (item.isMarkdown) {
    editItem(item)
  }
}

const editItem = (item) => {
  // 触发编辑文件事件
  window.dispatchEvent(new CustomEvent('edit-file', { 
    detail: { filePath: item.path } 
  }))
  close()
}

const deleteItem = async (item) => {
  if (!confirm(`确定要删除 "${item.name}" 吗?`)) return
  
  try {
    await api.delete('/api/files/delete', {
      data: { path: item.path }
    })
    
    // 重新加载当前目录
    refresh()
  } catch (err) {
    alert(err.response?.data?.error || '删除失败')
  }
}

const createItem = async () => {
  if (!createForm.value.name.trim()) return
  
  try {
    const itemPath = currentPath.value 
      ? `${currentPath.value}/${createForm.value.name}`
      : createForm.value.name
    
    await api.post('/api/files/create', {
      path: itemPath,
      type: createForm.value.type,
      content: createForm.value.type === 'file' ? '# 新文件\n\n内容...' : undefined
    })
    
    closeCreateDialog()
    refresh()
  } catch (err) {
    alert(err.response?.data?.error || '创建失败')
  }
}

const closeCreateDialog = () => {
  showCreateDialog.value = false
  createForm.value = {
    type: 'file',
    name: ''
  }
}

// 事件监听
const handleOpenFileTree = () => {
  show()
}

onMounted(() => {
  window.addEventListener('open-file-tree', handleOpenFileTree)
})

onUnmounted(() => {
  window.removeEventListener('open-file-tree', handleOpenFileTree)
})
</script>

<style scoped>
.file-tree-modal {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0,0,0,0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10000;
  padding: 20px;
}

.file-tree-container {
  background: white;
  border-radius: 12px;
  width: 100%;
  max-width: 600px;
  max-height: 80vh;
  display: flex;
  flex-direction: column;
  box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}

.file-tree-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20px;
  border-bottom: 1px solid #e2e8f0;
  background: #f7fafc;
}

.file-tree-header h3 {
  margin: 0;
  color: #2d3748;
}

.close-btn {
  background: none;
  border: none;
  font-size: 24px;
  cursor: pointer;
  color: #718096;
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
}

.close-btn:hover {
  background: #e2e8f0;
}

.file-tree-toolbar {
  padding: 16px 20px;
  border-bottom: 1px solid #e2e8f0;
  display: flex;
  justify-content: space-between;
  align-items: center;
  background: #f7fafc;
}

.current-path {
  font-size: 14px;
}

.path-label {
  color: #718096;
  margin-right: 8px;
}

.path-value {
  color: #2d3748;
  font-family: monospace;
  background: #e2e8f0;
  padding: 2px 6px;
  border-radius: 4px;
}

.toolbar-actions {
  display: flex;
  gap: 8px;
}

.toolbar-btn {
  padding: 6px 12px;
  background: white;
  border: 1px solid #e2e8f0;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
  transition: all 0.2s;
}

.toolbar-btn:hover:not(:disabled) {
  background: #e2e8f0;
}

.toolbar-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.file-tree-content {
  flex: 1;
  overflow-y: auto;
  padding: 20px;
}

.loading, .error {
  text-align: center;
  padding: 40px 20px;
  color: #718096;
}

.error {
  color: #e53e3e;
}

.file-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.file-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px;
  border-radius: 6px;
  cursor: pointer;
  transition: background 0.2s;
}

.file-item:hover {
  background: #f7fafc;
}

.file-icon {
  font-size: 16px;
  width: 20px;
  text-align: center;
}

.file-name {
  flex: 1;
  color: #2d3748;
  font-size: 14px;
}

.file-actions {
  display: flex;
  gap: 4px;
  opacity: 0;
  transition: opacity 0.2s;
}

.file-item:hover .file-actions {
  opacity: 1;
}

.action-btn {
  background: none;
  border: none;
  cursor: pointer;
  padding: 4px;
  border-radius: 4px;
  font-size: 12px;
}

.action-btn:hover {
  background: #e2e8f0;
}

.file-tree-footer {
  padding: 16px 20px;
  border-top: 1px solid #e2e8f0;
  text-align: center;
}

.create-btn {
  padding: 8px 16px;
  background: #4299e1;
  color: white;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 14px;
}

.create-btn:hover {
  background: #3182ce;
}

/* 创建对话框 */
.create-dialog {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0,0,0,0.5);
  display: flex;
  align-items: center;
  justify-content: center;
}

.dialog-content {
  background: white;
  padding: 24px;
  border-radius: 8px;
  width: 100%;
  max-width: 400px;
  margin: 20px;
}

.dialog-content h4 {
  margin: 0 0 16px 0;
  color: #2d3748;
}

.form-group {
  margin-bottom: 16px;
}

.form-group label {
  display: block;
  margin-bottom: 4px;
  color: #4a5568;
  font-size: 14px;
}

.form-group select,
.form-group input {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid #e2e8f0;
  border-radius: 4px;
  font-size: 14px;
}

.dialog-actions {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
  margin-top: 20px;
}

.dialog-actions button {
  padding: 8px 16px;
  border: 1px solid #e2e8f0;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
}

.dialog-actions button:last-child {
  background: #4299e1;
  color: white;
  border-color: #4299e1;
}

.dialog-actions button:last-child:hover:not(:disabled) {
  background: #3182ce;
}

.dialog-actions button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
</style>
EOF

含义: 创建文件树浏览组件,支持文件管理和导航

5. 创建 Markdown 编辑器组件

5.1 创建编辑器核心组件

bash
cat > docs/.vitepress/components/editor/MarkdownEditor.vue << 'EOF'
<template>
  <div class="markdown-editor-modal" v-if="isVisible" @click.self="handleBackdropClick">
    <div class="editor-container">
      <div class="editor-header">
        <div class="file-info">
          <h3>{{ fileName }}</h3>
          <div class="file-status">
            <span v-if="isDirty" class="dirty-indicator">● 未保存</span>
            <span v-if="lastSaved" class="last-saved">最后保存: {{ formatTime(lastSaved) }}</span>
          </div>
        </div>
        <div class="header-actions">
          <button 
            class="action-btn save-btn"
            @click="saveDraft"
            :disabled="!isDirty"
          >
            💾 保存草稿
          </button>
          <button 
            class="action-btn commit-btn"
            @click="commitFile"
            :disabled="!isDirty || isLoading"
          >
            ✅ 提交保存
          </button>
          <button class="action-btn close-btn" @click="handleClose">
            ✕ 关闭
          </button>
        </div>
      </div>

      <div class="editor-toolbar">
        <div class="toolbar-group">
          <button 
            class="toolbar-btn"
            @click="insertText('# ', '')"
            title="标题"
          >
            H1
          </button>
          <button 
            class="toolbar-btn"
            @click="insertText('**', '**')"
            title="加粗"
          >
            B
          </button>
          <button 
            class="toolbar-btn"
            @click="insertText('*', '*')"
            title="斜体"
          >
            I
          </button>
          <button 
            class="toolbar-btn"
            @click="insertText('`', '`')"
            title="代码"
          >
            `
          </button>
          <button 
            class="toolbar-btn"
            @click="insertText('[', '](url)')"
            title="链接"
          >
            🔗
          </button>
          <button 
            class="toolbar-btn"
            @click="insertText('- ', '')"
            title="列表"
          >

          </button>
        </div>
        <div class="toolbar-group">
          <button 
            class="toolbar-btn"
            @click="togglePreview"
            :class="{ active: showPreview }"
          >
            {{ showPreview ? '编辑' : '预览' }}
          </button>
        </div>
      </div>

      <div class="editor-content">
        <div class="editor-pane" v-show="!showPreview">
          <div 
            ref="editorContainer"
            class="monaco-editor-container"
          ></div>
        </div>
        
        <div class="preview-pane" v-show="showPreview">
          <div 
            class="preview-content"
            v-html="previewHtml"
          ></div>
        </div>
      </div>

      <div class="editor-footer">
        <div class="status-info">
          <span>行: {{ cursorPosition.line }}</span>
          <span>列: {{ cursorPosition.column }}</span>
          <span>字符: {{ characterCount }}</span>
        </div>
        <div class="commit-info" v-if="fileInfo?.lastModified">
          <span>最后修改: {{ fileInfo.lastModified.author.name }} - {{ formatTime(fileInfo.lastModified.date) }}</span>
        </div>
      </div>
    </div>

    <!-- 提交对话框 -->
    <div class="commit-dialog" v-if="showCommitDialog" @click.self="closeCommitDialog">
      <div class="dialog-content">
        <h4>提交更改</h4>
        <div class="form-group">
          <label>提交信息:</label>
          <input 
            v-model="commitMessage"
            type="text"
            placeholder="描述这次更改..."
            @keyup.enter="confirmCommit"
          />
        </div>
        <div class="dialog-actions">
          <button @click="closeCommitDialog">取消</button>
          <button @click="confirmCommit" :disabled="!commitMessage.trim() || isLoading">
            {{ isLoading ? '提交中...' : '确认提交' }}
          </button>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
import * as monaco from 'monaco-editor'
import { api } from '../../stores/auth.js'

// 响应式数据
const isVisible = ref(false)
const isLoading = ref(false)
const currentFilePath = ref('')
const originalContent = ref('')
const currentContent = ref('')
const fileInfo = ref(null)
const editor = ref(null)
const editorContainer = ref(null)
const isDirty = ref(false)
const lastSaved = ref(null)
const showPreview = ref(false)
const previewHtml = ref('')
const showCommitDialog = ref(false)
const commitMessage = ref('')
const cursorPosition = ref({ line: 1, column: 1 })
const fileHash = ref('');

// 计算属性
const fileName = computed(() => {
  if (!currentFilePath.value) return '新文件'
  return currentFilePath.value.split('/').pop() || '未知文件'
})

const characterCount = computed(() => {
  return currentContent.value.length
})

// 方法
const show = async (filePath) => {
  try {
    isVisible.value = true
    currentFilePath.value = filePath
    isLoading.value = true

    // 加载文件内容
    const response = await api.get('/api/files/content', {
      params: { path: filePath }
    })

    if (response.data.success) {
      originalContent.value = response.data.content
      currentContent.value = response.data.content
      fileInfo.value = response.data
      fileHash.value = response.data.lastModified?.hash || '';
      isDirty.value = false
      
      await nextTick()
      initializeEditor()
    }
  } catch (err) {
    alert(err.response?.data?.error || '加载文件失败')
    close()
  } finally {
    isLoading.value = false
  }
}

const close = () => {
  if (editor.value) {
    editor.value.dispose()
    editor.value = null
  }
  
  isVisible.value = false
  currentFilePath.value = ''
  originalContent.value = ''
  currentContent.value = ''
  fileInfo.value = null
  isDirty.value = false
  showPreview.value = false
  showCommitDialog.value = false
  commitMessage.value = ''
}

const initializeEditor = () => {
  if (!editorContainer.value) return

  // 配置 Monaco Editor
  editor.value = monaco.editor.create(editorContainer.value, {
    value: currentContent.value,
    language: 'markdown',
    theme: 'vs',
    automaticLayout: true,
    wordWrap: 'on',
    minimap: { enabled: false },
    fontSize: 14,
    lineNumbers: 'on',
    renderWhitespace: 'selection',
    tabSize: 2,
    insertSpaces: true
  })

  // 监听内容变化
  editor.value.onDidChangeModelContent(() => {
    const newContent = editor.value.getValue()
    currentContent.value = newContent
    isDirty.value = newContent !== originalContent.value
    updatePreview()
  })

  // 监听光标位置变化
  editor.value.onDidChangeCursorPosition((e) => {
    cursorPosition.value = {
      line: e.position.lineNumber,
      column: e.position.column
    }
  })

  // 初始化预览
  updatePreview()
}

const updatePreview = () => {
  if (!showPreview.value) return
  
  // 简单的 Markdown 转 HTML(实际应用中应该使用专业的 Markdown 解析器)
  let html = currentContent.value
    .replace(/^# (.*$)/gim, '<h1>$1</h1>')
    .replace(/^## (.*$)/gim, '<h2>$1</h2>')
    .replace(/^### (.*$)/gim, '<h3>$1</h3>')
    .replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>')
    .replace(/\*(.*)\*/gim, '<em>$1</em>')
    .replace(/`(.*)`/gim, '<code>$1</code>')
    .replace(/\n/gim, '<br>')
  
  previewHtml.value = html
}

const insertText = (before, after) => {
  if (!editor.value) return
  
  const selection = editor.value.getSelection()
  const selectedText = editor.value.getModel().getValueInRange(selection)
  
  editor.value.executeEdits('', [{
    range: selection,
    text: before + selectedText + after
  }])
  
  editor.value.focus()
}

const togglePreview = () => {
  showPreview.value = !showPreview.value
  if (showPreview.value) {
    updatePreview()
  }
}

const saveDraft = async () => {
  try {
    await api.post('/api/drafts/save', {
      path: currentFilePath.value,
      content: currentContent.value,
      metadata: {
        fileName: fileName.value,
        timestamp: new Date().toISOString()
      }
    })
    
    lastSaved.value = new Date()
    alert('草稿已保存')
  } catch (err) {
    alert(err.response?.data?.error || '保存草稿失败')
  }
}

const commitFile = () => {
  commitMessage.value = `更新文件: ${fileName.value}`;
  showCommitDialog.value = true;
}

const confirmCommit = async () => {
  if (!commitMessage.value.trim()) return
  
  try {
    isLoading.value = true;
    await api.post('/api/files/save', {
      path: currentFilePath.value,
      content: currentContent.value,
      commitMessage: commitMessage.value,
      isDraft: false,
      lastHash: fileHash.value // 新增:提交时带上文件hash
    });
    
    originalContent.value = currentContent.value
    isDirty.value = false
    lastSaved.value = new Date()
    closeCommitDialog()
    
    alert('文件已保存并提交')
  } catch (err) {
    if (err.response?.status === 409) {
      alert('冲突检测:文件已被他人修改,请刷新后合并您的更改!');
    } else {
      alert(err.response?.data?.error || '提交失败');
    }
  } finally {
    isLoading.value = false;
  }
}

const closeCommitDialog = () => {
  showCommitDialog.value = false
  commitMessage.value = ''
}

const handleClose = () => {
  if (isDirty.value) {
    if (!confirm('有未保存的更改,确定要关闭吗?')) {
      return
    }
  }
  close()
}

const handleBackdropClick = (event) => {
  if (event.target === event.currentTarget) {
    handleClose()
  }
}

const formatTime = (dateString) => {
  return new Date(dateString).toLocaleString('zh-CN')
}

// 监听预览模式变化
watch(showPreview, (newValue) => {
  if (newValue) {
    updatePreview()
  }
})

// 事件监听
const handleEditFile = (event) => {
  show(event.detail.filePath)
}

onMounted(() => {
  window.addEventListener('edit-file', handleEditFile)
})

onUnmounted(() => {
  window.removeEventListener('edit-file', handleEditFile)
  if (editor.value) {
    editor.value.dispose()
  }
})
</script>

<style scoped>
.markdown-editor-modal {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0,0,0,0.8);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10001;
  padding: 20px;
}

.editor-container {
  background: white;
  border-radius: 12px;
  width: 100%;
  height: 100%;
  max-width: 1200px;
  max-height: 90vh;
  display: flex;
  flex-direction: column;
  box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}

.editor-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px 20px;
  border-bottom: 1px solid #e2e8f0;
  background: #f7fafc;
}

.file-info h3 {
  margin: 0 0 4px 0;
  color: #2d3748;
  font-size: 18px;
}

.file-status {
  display: flex;
  gap: 12px;
  font-size: 12px;
}

.dirty-indicator {
  color: #e53e3e;
  font-weight: bold;
}

.last-saved {
  color: #718096;
}

.header-actions {
  display: flex;
  gap: 8px;
}

.action-btn {
  padding: 8px 12px;
  border: 1px solid #e2e8f0;
  border-radius: 6px;
  cursor: pointer;
  font-size: 12px;
  transition: all 0.2s;
  background: white;
}

.save-btn:hover:not(:disabled) {
  background: #edf2f7;
  border-color: #cbd5e0;
}

.commit-btn {
  background: #4299e1;
  color: white;
  border-color: #4299e1;
}

.commit-btn:hover:not(:disabled) {
  background: #3182ce;
}

.close-btn:hover {
  background: #fed7d7;
  border-color: #fcb3b3;
}

.action-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.editor-toolbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 20px;
  border-bottom: 1px solid #e2e8f0;
  background: #f7fafc;
}

.toolbar-group {
  display: flex;
  gap: 4px;
}

.toolbar-btn {
  padding: 6px 10px;
  border: 1px solid #e2e8f0;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
  background: white;
  transition: all 0.2s;
}

.toolbar-btn:hover {
  background: #e2e8f0;
}

.toolbar-btn.active {
  background: #4299e1;
  color: white;
  border-color: #4299e1;
}

.editor-content {
  flex: 1;
  display: flex;
  overflow: hidden;
}

.editor-pane, .preview-pane {
  flex: 1;
  overflow: hidden;
}

.monaco-editor-container {
  width: 100%;
  height: 100%;
}

.preview-content {
  padding: 20px;
  height: 100%;
  overflow-y: auto;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  line-height: 1.6;
}

.preview-content h1, .preview-content h2, .preview-content h3 {
  margin-top: 24px;
  margin-bottom: 16px;
  color: #2d3748;
}

.preview-content h1 {
  font-size: 24px;
  border-bottom: 1px solid #e2e8f0;
  padding-bottom: 8px;
}

.preview-content h2 {
  font-size: 20px;
}

.preview-content h3 {
  font-size: 16px;
}

.preview-content strong {
  font-weight: 600;
}

.preview-content code {
  background: #f1f5f9;
  padding: 2px 4px;
  border-radius: 3px;
  font-family: 'Fira Code', Consolas, monospace;
  font-size: 85%;
}

.editor-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 8px 20px;
  border-top: 1px solid #e2e8f0;
  background: #f7fafc;
  font-size: 12px;
  color: #718096;
}

.status-info {
  display: flex;
  gap: 16px;
}

/* 提交对话框 */
.commit-dialog {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0,0,0,0.5);
  display: flex;
  align-items: center;
  justify-content: center;
}

.dialog-content {
  background: white;
  padding: 24px;
  border-radius: 8px;
  width: 100%;
  max-width: 400px;
  margin: 20px;
}

.dialog-content h4 {
  margin: 0 0 16px 0;
  color: #2d3748;
}

.form-group {
  margin-bottom: 16px;
}

.form-group label {
  display: block;
  margin-bottom: 4px;
  color: #4a5568;
  font-size: 14px;
}

.form-group input {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid #e2e8f0;
  border-radius: 4px;
  font-size: 14px;
}

.dialog-actions {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
  margin-top: 20px;
}

.dialog-actions button {
  padding: 8px 16px;
  border: 1px solid #e2e8f0;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
}

.dialog-actions button:last-child {
  background: #4299e1;
  color: white;
  border-color: #4299e1;
}

.dialog-actions button:last-child:hover:not(:disabled) {
  background: #3182ce;
}

.dialog-actions button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* 响应式设计 */
@media (max-width: 768px) {
  .markdown-editor-modal {
    padding: 10px;
  }
  
  .editor-header {
    flex-direction: column;
    gap: 12px;
    align-items: flex-start;
  }
  
  .header-actions {
    width: 100%;
    justify-content: flex-end;
  }
  
  .action-btn {
    font-size: 11px;
    padding: 6px 8px;
  }
}
</style>
EOF

含义: 创建完整的 Markdown 编辑器,包含 Monaco Editor 集成和预览功能

6. 整合组件到 VitePress

6.1 修改 VitePress 配置以包含组件

bash
cat > docs/.vitepress/theme/index.js << 'EOF'
import DefaultTheme from 'vitepress/theme'
import HamburgerMenu from '../components/editor/HamburgerMenu.vue'
import FileTree from '../components/editor/FileTree.vue'
import MarkdownEditor from '../components/editor/MarkdownEditor.vue'

export default {
  ...DefaultTheme,
  enhanceApp({ app }) {
    app.component('HamburgerMenu', HamburgerMenu)
    app.component('FileTree', FileTree)
    app.component('MarkdownEditor', MarkdownEditor)
  }
}
EOF

含义: 注册自定义组件到 VitePress 主题

6.2 在布局中添加编辑器组件

bash
cat > docs/.vitepress/theme/Layout.vue << 'EOF'
<template>
  <Layout>
    <!-- 汉堡菜单 -->
    <HamburgerMenu />
    
    <!-- 文件树 -->
    <FileTree />
    
    <!-- Markdown 编辑器 -->
    <MarkdownEditor />
  </Layout>
</template>

<script setup>
import { Layout } from 'vitepress/theme'
import HamburgerMenu from '../components/editor/HamburgerMenu.vue'
import FileTree from '../components/editor/FileTree.vue'
import MarkdownEditor from '../components/editor/MarkdownEditor.vue'
</script>
EOF

含义: 创建自定义布局,包含所有编辑器组件

7. 测试前端功能

7.1 启动 VitePress 开发服务器

bash
npm run docs:dev

含义: 启动前端开发服务器

7.2 同时启动 API 服务器

bash
# 在另一个终端
cd api
npm run dev

含义: 启动后端 API 服务器

8. 提交第四阶段代码

8.1 提交前端界面代码

bash
git add .
git commit -m "feat: 实现前端编辑界面和组件

- 创建汉堡菜单用户界面
- 实现文件树浏览和管理
- 集成Monaco Editor编辑器
- 添加认证状态管理
- 支持草稿保存和文件提交
- 实现响应式设计和预览功能"

含义: 提交第四阶段的所有前端代码

第四阶段总结:

  • ✅ 创建了完整的汉堡菜单界面
  • ✅ 实现了用户认证状态管理
  • ✅ 开发了文件树浏览组件
  • ✅ 集成了 Monaco Editor 编辑器
  • ✅ 添加了草稿保存和提交功能
  • ✅ 支持 Markdown 预览和工具栏

下一步预告: 第五阶段将实现 Git 集成和冲突检测功能。

请测试当前的前端功能是否正常工作,然后我们继续第五阶段的开发

第五阶段:Git 集成与冲突检测

1. 自动化 Git 提交与推送

1.1 配置自动推送脚本

bash
cat > api/utils/autoPush.js << 'EOF'
const simpleGit = require('simple-git');
const path = require('path');

const git = simpleGit(process.env.WORK_DIR);

async function autoPush() {
  try {
    await git.fetch();
    await git.pull();
    await git.push();
    console.log('✅ 自动推送到远程仓库成功');
  } catch (error) {
    console.error('❌ 自动推送失败:', error);
  }
}

module.exports = autoPush;
EOF

含义: 创建自动推送工具,提交后自动同步到远程仓库

1.2 在文件保存 API 中集成自动推送

js
// ...api/routes/files.js 文件保存API...
const autoPush = require('../utils/autoPush');

router.post('/save', authenticateToken, requireEditPermission, async (req, res) => {
  // ...existing code...
  if (!isDraft) {
    // ...existing code...
    await gitManager.commit(message, author);
    await autoPush(); // 新增:自动推送到远程仓库
  }
  // ...existing code...
});

含义: 文件提交后自动推送到远程仓库,确保线上与本地同步

2. 冲突检测与处理

2.1 检查文件是否有冲突

js
// ...api/routes/files.js 文件保存API...
const git = require('simple-git')(process.env.WORK_DIR);

async function checkConflict(filePath, clientHash) {
  const log = await git.log(['-1', '--', filePath]);
  if (log.latest && clientHash && log.latest.hash !== clientHash) {
    return true; // 有冲突
  }
  return false;
}

router.post('/save', authenticateToken, requireEditPermission, async (req, res) => {
  const { path: filePath, content, commitMessage, isDraft = false, lastHash } = req.body;
  // ...existing code...
  if (!isDraft) {
    // 冲突检测
    const hasConflict = await checkConflict(filePath, lastHash);
    if (hasConflict) {
      return res.status(409).json({ error: '文件已被他人修改,请刷新后合并您的更改!' });
    }
    // ...existing code...
  }
  // ...existing code...
});

含义: 保存文件时检测 Git 最新版本与客户端版本是否一致,防止覆盖他人更改

3. 前端冲突提示与合并

3.1 编辑器组件增加冲突处理

js
// ...MarkdownEditor.vue...
const fileHash = ref('');

const show = async (filePath) => {
  // ...existing code...
  if (response.data.success) {
    // ...existing code...
    fileHash.value = response.data.lastModified?.hash || '';
    // ...existing code...
  }
  // ...existing code...
}

const commitFile = () => {
  commitMessage.value = `更新文件: ${fileName.value}`;
  showCommitDialog.value = true;
}

const confirmCommit = async () => {
  if (!commitMessage.value.trim()) return
  
  try {
    isLoading.value = true;
    await api.post('/api/files/save', {
      path: currentFilePath.value,
      content: currentContent.value,
      commitMessage: commitMessage.value,
      isDraft: false,
      lastHash: fileHash.value // 新增:提交时带上文件hash
    });
    
    originalContent.value = currentContent.value
    isDirty.value = false
    lastSaved.value = new Date()
    closeCommitDialog()
    
    alert('文件已保存并提交')
  } catch (err) {
    if (err.response?.status === 409) {
      alert('冲突检测:文件已被他人修改,请刷新后合并您的更改!');
    } else {
      alert(err.response?.data?.error || '提交失败');
    }
  } finally {
    isLoading.value = false;
  }
}

含义: 前端编辑器提交时带上文件版本 hash,后端检测冲突并提示用户

4. 文件历史与版本回溯

4.1 文件历史 API 前端调用

js
// ...MarkdownEditor.vue...
const fileHistory = ref([]);

const loadHistory = async () => {
  if (!currentFilePath.value) return;
  try {
    const response = await api.get('/api/files/history', {
      params: { path: currentFilePath.value, limit: 10 }
    });
    if (response.data.success) {
      fileHistory.value = response.data.history;
    }
  } catch (err) {
    fileHistory.value = [];
  }
}

onMounted(() => {
  // ...existing code...
  loadHistory();
});

含义: 加载文件历史,支持版本回溯和对比

5. 提交第五阶段代码

5.1 提交 Git 集成与冲突检测功能

bash
git add .
git commit -m "feat: 集成Git自动推送与冲突检测

- 文件保存自动推送到远程仓库
- 保存时检测文件版本冲突
- 编辑器前端冲突提示
- 支持文件历史查询和版本回溯"

含义: 提交第五阶段所有代码

5.2 合并到主分支

bash
# 切换到主分支
git checkout main

# 合并功能分支
git merge feature/online-editor

# 推送到远程
git push origin main

含义: 将完整功能合并到主分支

6. 部署到生产环境

6.1 在服务器执行部署

bash
# 连接到服务器
ssh webnote@43.159.60.248

# 进入项目目录
cd /home/webnote/webnote/source/WebNote

# 执行部署
./deploy.sh

含义: 在生产服务器执行自动化部署

6.2 设置定时监控

bash
# 添加到 crontab
(crontab -l 2>/dev/null; echo "0 */6 * * * /home/webnote/webnote/source/WebNote/monitor.sh check") | crontab -
(crontab -l 2>/dev/null; echo "0 2 * * * /home/webnote/webnote/source/WebNote/monitor.sh cleanup") | crontab -
(crontab -l 2>/dev/null; echo "0 3 * * 0 /home/webnote/webnote/source/WebNote/monitor.sh backup") | crontab -

含义: 设置定时任务:每6小时检查服务,每天凌晨2点清理日志,每周日凌晨3点备份数据

🎉 项目完成总结

完整功能清单

用户认证系统

  • JWT 令牌认证
  • 角色权限控制(admin/editor)
  • 安全的密码加密存储

文件管理功能

  • 可视化文件树浏览
  • 文件创建、编辑、删除、移动
  • 安全路径验证和权限控制

在线编辑器

  • Monaco Editor(VS Code 级别)
  • Markdown 语法高亮
  • 实时预览功能
  • 工具栏快捷操作

版本控制集成

  • 自动 Git 提交和推送
  • 文件历史查询
  • 冲突检测和处理
  • 提交信息自定义

草稿系统

  • 自动草稿保存
  • 用户专属草稿管理
  • 防止工作丢失

实时协作

  • WebSocket 多用户同步
  • 在线用户显示
  • 编辑状态实时更新
  • 光标位置和选择同步

生产部署

  • PM2 进程管理
  • Nginx 反向代理
  • 自动化部署脚本
  • 监控和维护工具

技术架构

前端 (VitePress + Vue3)
├── 汉堡菜单界面
├── 文件树浏览器
├── Monaco 编辑器
├── 实时协作面板
└── 认证状态管理

后端 (Express + Node.js)
├── JWT 认证系统
├── 文件操作 API
├── Git 版本控制
├── 草稿管理
├── WebSocket 协作
└── 安全中间件

基础设施
├── PM2 进程管理
├── Nginx 反向代理
├── Git 自动化
└── 监控维护

安全特性

  • 🔐 JWT 令牌认证
  • 🛡️ 路径遍历攻击防护
  • ⚡ API 速率限制
  • 🔒 CORS 跨域控制
  • 📝 输入验证和清理

性能优化

  • 🚀 WebSocket 实时通信
  • 💾 本地存储状态管理
  • 🔄 自动重连机制
  • 📊 进程监控和重启
  • 🧹 日志轮转和清理

部署说明

  1. 开发环境: npm run docs:dev + cd api && npm run dev
  2. 生产部署: 执行 ./deploy.sh 一键部署
  3. 服务监控: pm2 monit 查看状态
  4. 日志查看: pm2 logs webnote-api

用户体验

  • 📱 响应式设计,支持移动端
  • ⚡ 实时预览和协作
  • 💾 自动保存防止丢失
  • 🎯 直观的文件管理界面
  • 🤝 多用户协作提示

🎊 恭喜!WebNote 在线编辑功能开发完成!

现在您拥有了一个功能完整、安全可靠、支持多用户协作的 VitePress 在线编辑系统。用户可以通过网页直接编辑 Markdown 文件,系统会自动处理版本控制、冲突检测和部署更新。