首页 / 应用软件 / 一步步实现,为WordPress打造内嵌的在线Markdown笔记与共享协作工具

一步步实现,为WordPress打造内嵌的在线Markdown笔记与共享协作工具

一步步实现:为WordPress打造内嵌的在线Markdown笔记与共享协作工具

引言:为什么WordPress需要内嵌Markdown协作工具?

在当今数字化工作环境中,内容创作和团队协作的效率直接影响着项目的成功。WordPress作为全球最流行的内容管理系统,虽然拥有强大的发布功能,但在实时协作和结构化笔记方面却存在明显不足。传统的WordPress编辑器对于需要频繁协作的技术团队、内容创作者和教育机构来说,往往显得笨重且效率低下。

Markdown作为一种轻量级标记语言,以其简洁的语法和清晰的格式呈现,已经成为技术文档、笔记和协作内容的首选格式。将Markdown编辑与实时协作功能集成到WordPress中,不仅可以提升内容创作效率,还能为团队提供无缝的协作体验。

本文将详细介绍如何通过WordPress代码二次开发,构建一个功能完整的内嵌式Markdown笔记与共享协作工具,让您的WordPress网站具备类似Notion、语雀等现代协作平台的核心功能。

第一部分:项目规划与技术选型

1.1 功能需求分析

在开始开发之前,我们需要明确工具的核心功能需求:

  1. Markdown编辑器:支持实时预览、语法高亮、常用格式快捷键
  2. 实时协作功能:多用户同时编辑、光标位置显示、更改实时同步
  3. 笔记管理:文件夹/标签分类、全文搜索、版本历史
  4. 权限控制系统:基于角色的访问控制、分享链接设置
  5. 数据存储与同步:可靠的数据存储机制、离线编辑支持
  6. WordPress集成:用户系统集成、主题样式兼容、插件化部署

1.2 技术架构设计

为了实现上述功能,我们采用以下技术栈:

  • 前端框架:React + TypeScript(提供良好的组件化开发和类型安全)
  • 实时通信:WebSocket(用于实时协作同步)
  • Markdown解析:Marked.js + highlight.js(轻量且功能强大)
  • 编辑器组件:CodeMirror 6(现代化、可扩展的代码编辑器)
  • 后端框架:WordPress REST API扩展 + 自定义数据库表
  • 数据同步:Operational Transformation(OT)算法解决冲突
  • 存储方案:MySQL自定义表 + 本地存储备份

1.3 开发环境搭建

首先,我们需要设置开发环境:

# 创建插件目录结构
mkdir wp-markdown-collab
cd wp-markdown-collab

# 初始化插件主文件
touch wp-markdown-collab.php

# 创建核心目录
mkdir includes
mkdir admin
mkdir public
mkdir assets
mkdir build

# 初始化package.json用于前端构建
npm init -y

# 安装前端依赖
npm install react react-dom typescript @types/react @types/react-dom
npm install codemirror @uiw/react-codemirror marked highlight.js
npm install socket.io-client
npm install --save-dev webpack webpack-cli babel-loader @babel/core @babel/preset-react @babel/preset-typescript

第二部分:构建Markdown编辑器核心

2.1 创建基础编辑器组件

让我们从构建Markdown编辑器开始。首先创建编辑器React组件:

// assets/js/components/MarkdownEditor.tsx
import React, { useState, useEffect } from 'react';
import CodeMirror from '@uiw/react-codemirror';
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
import { languages } from '@codemirror/language-data';
import { oneDark } from '@codemirror/theme-one-dark';
import { EditorView } from '@codemirror/view';
import { marked } from 'marked';
import hljs from 'highlight.js';
import 'highlight.js/styles/github-dark.css';

interface MarkdownEditorProps {
  content: string;
  onChange: (content: string) => void;
  readOnly?: boolean;
}

const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
  content,
  onChange,
  readOnly = false
}) => {
  const [preview, setPreview] = useState<string>('');
  const [activeTab, setActiveTab] = useState<'edit' | 'preview' | 'split'>('split');
  
  // 配置marked解析器
  marked.setOptions({
    highlight: function(code, lang) {
      if (lang && hljs.getLanguage(lang)) {
        return hljs.highlight(code, { language: lang }).value;
      }
      return hljs.highlightAuto(code).value;
    },
    breaks: true,
    gfm: true
  });
  
  // 更新预览内容
  useEffect(() => {
    const renderPreview = async () => {
      const html = await marked.parse(content || '');
      setPreview(html as string);
    };
    renderPreview();
  }, [content]);
  
  // CodeMirror扩展配置
  const extensions = [
    markdown({ base: markdownLanguage, codeLanguages: languages }),
    EditorView.lineWrapping,
    EditorView.theme({
      "&": { height: "100%" },
      ".cm-scroller": { overflow: "auto", fontFamily: "'Fira Code', monospace" },
      ".cm-content": { padding: "10px 0" }
    })
  ];
  
  return (
    <div className="wp-md-editor-container">
      <div className="editor-toolbar">
        <button 
          className={`tab-btn ${activeTab === 'edit' ? 'active' : ''}`}
          onClick={() => setActiveTab('edit')}
        >
          编辑
        </button>
        <button 
          className={`tab-btn ${activeTab === 'preview' ? 'active' : ''}`}
          onClick={() => setActiveTab('preview')}
        >
          预览
        </button>
        <button 
          className={`tab-btn ${activeTab === 'split' ? 'active' : ''}`}
          onClick={() => setActiveTab('split')}
        >
          分屏
        </button>
        
        <div className="toolbar-actions">
          <button className="toolbar-btn" title="加粗">B</button>
          <button className="toolbar-btn" title="斜体">I</button>
          <button className="toolbar-btn" title="链接">🔗</button>
          <button className="toolbar-btn" title="代码块">{"</>"}</button>
          <button className="toolbar-btn" title="图片">🖼️</button>
        </div>
      </div>
      
      <div className={`editor-content ${activeTab}`}>
        {(activeTab === 'edit' || activeTab === 'split') && (
          <div className={`editor-pane ${activeTab === 'split' ? 'half' : 'full'}`}>
            <CodeMirror
              value={content}
              height="100%"
              theme={oneDark}
              extensions={extensions}
              onChange={(value) => onChange(value)}
              readOnly={readOnly}
              basicSetup={{
                lineNumbers: true,
                highlightActiveLineGutter: true,
                bracketMatching: true,
                closeBrackets: true,
                autocompletion: true,
                rectangularSelection: true,
                crosshairCursor: true,
                highlightActiveLine: true,
                foldGutter: true,
                dropCursor: true,
                allowMultipleSelections: true,
                indentOnInput: true,
                syntaxHighlighting: true,
                tabSize: 2
              }}
            />
          </div>
        )}
        
        {(activeTab === 'preview' || activeTab === 'split') && (
          <div className={`preview-pane ${activeTab === 'split' ? 'half' : 'full'}`}>
            <div 
              className="markdown-preview"
              dangerouslySetInnerHTML={{ __html: preview }}
            />
          </div>
        )}
      </div>
    </div>
  );
};

export default MarkdownEditor;

2.2 实现编辑器工具栏功能

接下来,我们需要为编辑器添加实用的工具栏功能:

// assets/js/components/EditorToolbar.tsx
import React from 'react';

interface EditorToolbarProps {
  onFormatAction: (action: string, value?: string) => void;
  onSave: () => void;
  onExport: (format: 'html' | 'pdf' | 'md') => void;
  isCollaborating: boolean;
  collaborators: Array<{id: number, name: string, color: string}>;
}

const EditorToolbar: React.FC<EditorToolbarProps> = ({
  onFormatAction,
  onSave,
  onExport,
  isCollaborating,
  collaborators
}) => {
  
  const handleFormatClick = (action: string) => {
    const actions: Record<string, {prefix: string, suffix: string, placeholder?: string}> = {
      bold: { prefix: '**', suffix: '**', placeholder: '加粗文字' },
      italic: { prefix: '*', suffix: '*', placeholder: '斜体文字' },
      link: { prefix: '[', suffix: '](url)', placeholder: '链接文字' },
      image: { prefix: '![', suffix: '](image-url)', placeholder: '图片描述' },
      code: { prefix: '`', suffix: '`', placeholder: '代码' },
      codeBlock: { prefix: '```n', suffix: 'n```', placeholder: '代码块' },
      quote: { prefix: '> ', suffix: '', placeholder: '引用文字' },
      list: { prefix: '- ', suffix: '', placeholder: '列表项' },
      numberedList: { prefix: '1. ', suffix: '', placeholder: '列表项' },
      heading1: { prefix: '# ', suffix: '', placeholder: '一级标题' },
      heading2: { prefix: '## ', suffix: '', placeholder: '二级标题' },
      heading3: { prefix: '### ', suffix: '', placeholder: '三级标题' },
    };
    
    onFormatAction(action, actions[action]?.placeholder);
  };
  
  return (
    <div className="editor-toolbar-extended">
      <div className="toolbar-section">
        <div className="format-buttons">
          <button onClick={() => handleFormatClick('heading1')} title="标题1">H1</button>
          <button onClick={() => handleFormatClick('heading2')} title="标题2">H2</button>
          <button onClick={() => handleFormatClick('heading3')} title="标题3">H3</button>
          <div className="separator"></div>
          <button onClick={() => handleFormatClick('bold')} title="加粗">
            <strong>B</strong>
          </button>
          <button onClick={() => handleFormatClick('italic')} title="斜体">
            <em>I</em>
          </button>
          <button onClick={() => handleFormatClick('link')} title="链接">🔗</button>
          <button onClick={() => handleFormatClick('image')} title="图片">🖼️</button>
          <div className="separator"></div>
          <button onClick={() => handleFormatClick('code')} title="行内代码">
            {"</>"}
          </button>
          <button onClick={() => handleFormatClick('codeBlock')} title="代码块">
            {"{ }"}
          </button>
          <button onClick={() => handleFormatClick('quote')} title="引用">❝</button>
          <button onClick={() => handleFormatClick('list')} title="无序列表">•</button>
          <button onClick={() => handleFormatClick('numberedList')} title="有序列表">1.</button>
        </div>
      </div>
      
      <div className="toolbar-section">
        <div className="action-buttons">
          <button onClick={onSave} className="save-btn">
            💾 保存
          </button>
          
          <div className="export-dropdown">
            <button className="export-btn">📥 导出</button>
            <div className="export-menu">
              <button onClick={() => onExport('html')}>HTML</button>
              <button onClick={() => onExport('pdf')}>PDF</button>
              <button onClick={() => onExport('md')}>Markdown</button>
            </div>
          </div>
          
          {isCollaborating && (
            <div className="collaborators-indicator">
              <span className="collab-icon">👥</span>
              <span className="collab-count">{collaborators.length + 1}</span>
              <div className="collaborators-list">
                {collaborators.map(collab => (
                  <div key={collab.id} className="collaborator">
                    <span 
                      className="user-avatar" 
                      style={{backgroundColor: collab.color}}
                    >
                      {collab.name.charAt(0)}
                    </span>
                    <span className="user-name">{collab.name}</span>
                  </div>
                ))}
              </div>
            </div>
          )}
        </div>
      </div>
    </div>
  );
};

export default EditorToolbar;

第三部分:实现实时协作系统

3.1 WebSocket服务器集成

实时协作需要WebSocket服务器来处理实时通信。我们将在WordPress中集成WebSocket功能:

<?php
// includes/class-websocket-server.php

class WP_Markdown_WebSocket_Server {
    
    private $server;
    private $clients;
    private $documents;
    
    public function __construct() {
        $this->clients = new SplObjectStorage;
        $this->documents = [];
        
        add_action('init', [$this, 'init_websocket']);
    }
    
    public function init_websocket() {
        // 检查是否应该启动WebSocket服务器
        if (defined('DOING_AJAX') && DOING_AJAX) {
            return;
        }
        
        // 创建WebSocket服务器
        $this->server = new RatchetApp(
            get_bloginfo('name'),
            8080,
            '0.0.0.0'
        );
        
        // 注册消息处理类
        $this->server->route('/collab', new CollaborationHandler(), ['*']);
        
        // 在后台进程中运行服务器
        if (php_sapi_name() === 'cli') {
            $this->server->run();
        }
    }
}

class CollaborationHandler implements RatchetMessageComponentInterface {
    
    protected $clients;
    protected $documents;
    
    public function __construct() {
        $this->clients = new SplObjectStorage;
        $this->documents = [];
    }
    
    public function onOpen(RatchetConnectionInterface $conn) {
        $this->clients->attach($conn);
        error_log("New connection: {$conn->resourceId}");
        
        // 发送欢迎消息
        $conn->send(json_encode([
            'type' => 'welcome',
            'message' => 'Connected to collaboration server',
            'clientId' => $conn->resourceId
        ]));
    }
    
    public function onMessage(RatchetConnectionInterface $from, $msg) {
        $data = json_decode($msg, true);
        
        if (!$data || !isset($data['type'])) {
            return;
        }
        
        switch ($data['type']) {
            case 'join_document':
                $this->handleJoinDocument($from, $data);
                break;
                
            case 'text_update':
                $this->handleTextUpdate($from, $data);
                break;
                
            case 'cursor_move':
                $this->handleCursorMove($from, $data);
                break;
                
            case 'selection_change':
                $this->handleSelectionChange($from, $data);
                break;
        }
    }
    
    private function handleJoinDocument($conn, $data) {
        $docId = $data['documentId'];
        $userId = $data['userId'];
        $userName = $data['userName'];
        
        if (!isset($this->documents[$docId])) {
            $this->documents[$docId] = [
                'content' => '',
                'clients' => [],
                'version' => 0
            ];
        }
        
        // 存储客户端信息
        $this->documents[$docId]['clients'][$conn->resourceId] = [
            'userId' => $userId,
            'userName' => $userName,
            'cursor' => null,
            'selection' => null,
            'color' => $this->generateUserColor($userId)
        ];
        
        // 发送当前文档状态给新用户
        $conn->send(json_encode([
            'type' => 'document_state',
            'documentId' => $docId,
            'content' => $this->documents[$docId]['content'],
            'version' => $this->documents[$docId]['version'],
            'clients' => $this->documents[$docId]['clients']
        ]));
        
        // 通知其他用户有新用户加入
        $this->broadcastToDocument($docId, $conn, [
            'type' => 'user_joined',
            'clientId' => $conn->resourceId,
            'userId' => $userId,
            'userName' => $userName,
            'color' => $this->documents[$docId]['clients'][$conn->resourceId]['color']
        ]);
    }
    
    private function handleTextUpdate($conn, $data) {
        $docId = $data['documentId'];
        $operations = $data['operations'];
        $clientVersion = $data['version'];
        
        if (!isset($this->documents[$docId])) {
            return;
        }
        
        $serverVersion = $this->documents[$docId]['version'];
        
        // 检查版本冲突
        if ($clientVersion !== $serverVersion) {
            // 发送冲突解决请求
            $conn->send(json_encode([
                'type' => 'version_conflict',
                'serverVersion' => $serverVersion,
                'serverContent' => $this->documents[$docId]['content']
            ]));
            return;
        }
        
        // 应用操作到文档
        $this->applyOperations($docId, $operations);
        
        // 增加版本号
        $this->documents[$docId]['version']++;
        
        // 广播更新给其他用户
        $this->broadcastToDocument($docId, $conn, [
            'type' => 'text_updated',
            'operations' => $operations,
            'version' => $this->documents[$docId]['version'],

resourceId,

        'userId' => $this->documents[$docId]['clients'][$conn->resourceId]['userId']
    ]);
}

private function applyOperations($docId, $operations) {
    $content = $this->documents[$docId]['content'];
    
    foreach ($operations as $op) {
        if ($op['type'] === 'insert') {
            $position = $op['position'];
            $text = $op['text'];
            $content = substr($content, 0, $position) . $text . substr($content, $position);
        } elseif ($op['type'] === 'delete') {
            $position = $op['position'];
            $length = $op['length'];
            $content = substr($content, 0, $position) . substr($content, $position + $length);
        }
    }
    
    $this->documents[$docId]['content'] = $content;
}

private function broadcastToDocument($docId, $excludeConn, $message) {
    foreach ($this->clients as $client) {
        if ($client !== $excludeConn && 
            isset($this->documents[$docId]['clients'][$client->resourceId])) {
            $client->send(json_encode($message));
        }
    }
}

private function generateUserColor($userId) {
    $colors = [
        '#FF6B6B', '#4ECDC4', '#FFD166', '#06D6A0',
        '#118AB2', '#073B4C', '#EF476F', '#7209B7'
    ];
    return $colors[abs(crc32($userId)) % count($colors)];
}

public function onClose(RatchetConnectionInterface $conn) {
    // 从所有文档中移除客户端
    foreach ($this->documents as $docId => $document) {
        if (isset($document['clients'][$conn->resourceId])) {
            $userInfo = $document['clients'][$conn->resourceId];
            
            // 通知其他用户该用户已离开
            $this->broadcastToDocument($docId, $conn, [
                'type' => 'user_left',
                'clientId' => $conn->resourceId,
                'userId' => $userInfo['userId']
            ]);
            
            unset($this->documents[$docId]['clients'][$conn->resourceId]);
        }
    }
    
    $this->clients->detach($conn);
    error_log("Connection closed: {$conn->resourceId}");
}

public function onError(RatchetConnectionInterface $conn, Exception $e) {
    error_log("Error: {$e->getMessage()}");
    $conn->close();
}

}


### 3.2 前端实时协作客户端

现在创建前端的WebSocket客户端来处理实时协作:

// assets/js/services/CollaborationClient.ts
import { io, Socket } from 'socket.io-client';

export interface Operation {
type: 'insert' | 'delete';
position: number;
text?: string;
length?: number;
clientId?: number;
timestamp: number;
}

export interface ClientInfo {
userId: number;
userName: string;
color: string;
cursor: { line: number; ch: number } | null;
selection: { from: number; to: number } | null;
}

export interface DocumentState {
content: string;
version: number;
clients: Record<number, ClientInfo>;
}

class CollaborationClient {
private socket: Socket | null = null;
private documentId: string | null = null;
private userId: number;
private userName: string;
private isConnected = false;

private operationsBuffer: Operation[] = [];
private lastSentVersion = 0;

// 事件监听器
private listeners: {

textUpdate: ((operations: Operation[], version: number) => void)[];
userJoined: ((clientId: number, userInfo: ClientInfo) => void)[];
userLeft: ((clientId: number, userId: number) => void)[];
cursorMove: ((clientId: number, cursor: any) => void)[];
selectionChange: ((clientId: number, selection: any) => void)[];
connectionChange: ((connected: boolean) => void)[];

} = {

textUpdate: [],
userJoined: [],
userLeft: [],
cursorMove: [],
selectionChange: [],
connectionChange: []

};

constructor(userId: number, userName: string) {

this.userId = userId;
this.userName = userName;

}

connect(serverUrl: string): Promise<void> {

return new Promise((resolve, reject) => {
  this.socket = io(serverUrl, {
    path: '/collab',
    transports: ['websocket', 'polling'],
    reconnection: true,
    reconnectionAttempts: 5,
    reconnectionDelay: 1000
  });

  this.socket.on('connect', () => {
    this.isConnected = true;
    this.notifyConnectionChange(true);
    console.log('Connected to collaboration server');
    resolve();
  });

  this.socket.on('disconnect', () => {
    this.isConnected = false;
    this.notifyConnectionChange(false);
    console.log('Disconnected from collaboration server');
  });

  this.socket.on('connect_error', (error) => {
    console.error('Connection error:', error);
    reject(error);
  });

  // 注册消息处理器
  this.setupMessageHandlers();
});

}

private setupMessageHandlers() {

if (!this.socket) return;

this.socket.on('welcome', (data: any) => {
  console.log('Server welcome:', data.message);
});

this.socket.on('document_state', (data: any) => {
  console.log('Received document state, version:', data.version);
  // 这里可以触发文档状态更新事件
});

this.socket.on('text_updated', (data: any) => {
  console.log('Text updated by client:', data.clientId);
  this.listeners.textUpdate.forEach(callback => 
    callback(data.operations, data.version)
  );
});

this.socket.on('user_joined', (data: any) => {
  console.log('User joined:', data.userName);
  this.listeners.userJoined.forEach(callback => 
    callback(data.clientId, {
      userId: data.userId,
      userName: data.userName,
      color: data.color,
      cursor: null,
      selection: null
    })
  );
});

this.socket.on('user_left', (data: any) => {
  console.log('User left:', data.userId);
  this.listeners.userLeft.forEach(callback => 
    callback(data.clientId, data.userId)
  );
});

this.socket.on('cursor_moved', (data: any) => {
  this.listeners.cursorMove.forEach(callback => 
    callback(data.clientId, data.cursor)
  );
});

this.socket.on('selection_changed', (data: any) => {
  this.listeners.selectionChange.forEach(callback => 
    callback(data.clientId, data.selection)
  );
});

this.socket.on('version_conflict', (data: any) => {
  console.warn('Version conflict detected');
  this.handleVersionConflict(data.serverVersion, data.serverContent);
});

}

joinDocument(documentId: string, initialContent: string = ''): void {

this.documentId = documentId;

if (!this.socket || !this.isConnected) {
  console.error('Not connected to server');
  return;
}

this.socket.emit('join_document', {
  documentId,
  userId: this.userId,
  userName: this.userName,
  initialContent
});

}

sendOperations(operations: Operation[], currentVersion: number): void {

if (!this.socket || !this.documentId || !this.isConnected) {
  // 缓存操作,等待连接恢复
  this.operationsBuffer.push(...operations);
  return;
}

this.socket.emit('text_update', {
  documentId: this.documentId,
  operations,
  version: currentVersion,
  clientId: this.socket.id
});

this.lastSentVersion = currentVersion;

}

sendCursorMove(cursor: { line: number; ch: number }): void {

if (!this.socket || !this.documentId || !this.isConnected) return;

this.socket.emit('cursor_move', {
  documentId: this.documentId,
  cursor,
  clientId: this.socket.id
});

}

sendSelectionChange(selection: { from: number; to: number }): void {

if (!this.socket || !this.documentId || !this.isConnected) return;

this.socket.emit('selection_change', {
  documentId: this.documentId,
  selection,
  clientId: this.socket.id
});

}

private handleVersionConflict(serverVersion: number, serverContent: string): void {

// 这里实现OT冲突解决算法
console.log('Resolving version conflict:', {
  serverVersion,
  clientVersion: this.lastSentVersion,
  serverContentLength: serverContent.length
});

// 触发冲突解决事件
// 实际实现中需要更复杂的OT算法

}

// 事件监听管理
onTextUpdate(callback: (operations: Operation[], version: number) => void): void {

this.listeners.textUpdate.push(callback);

}

onUserJoined(callback: (clientId: number, userInfo: ClientInfo) => void): void {

this.listeners.userJoined.push(callback);

}

onUserLeft(callback: (clientId: number, userId: number) => void): void {

this.listeners.userLeft.push(callback);

}

onCursorMove(callback: (clientId: number, cursor: any) => void): void {

this.listeners.cursorMove.push(callback);

}

onSelectionChange(callback: (clientId: number, selection: any) => void): void {

this.listeners.selectionChange.push(callback);

}

onConnectionChange(callback: (connected: boolean) => void): void {

this.listeners.connectionChange.push(callback);

}

private notifyConnectionChange(connected: boolean): void {

this.listeners.connectionChange.forEach(callback => callback(connected));

}

disconnect(): void {

if (this.socket) {
  this.socket.disconnect();
  this.socket = null;
}
this.isConnected = false;
this.documentId = null;

}

getConnectionStatus(): boolean {

return this.isConnected;

}
}

export default CollaborationClient;


## 第四部分:WordPress后端集成

### 4.1 创建数据库表结构

我们需要创建自定义数据库表来存储Markdown笔记和相关数据:

<?php
// includes/class-database.php

class WP_Markdown_Notes_DB {


private static $instance = null;
private $charset_collate;

public static function get_instance() {
    if (null === self::$instance) {
        self::$instance = new self();
    }
    return self::$instance;
}

private function __construct() {
    global $wpdb;
    $this->charset_collate = $wpdb->get_charset_collate();
    
    register_activation_hook(__FILE__, [$this, 'create_tables']);
    add_action('plugins_loaded', [$this, 'check_tables']);
}

public function create_tables() {
    global $wpdb;
    
    require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
    
    $tables = [
        $this->create_notes_table(),
        $this->create_note_versions_table(),
        $this->create_collaborators_table(),
        $this->create_folders_table(),
        $this->create_tags_table(),
        $this->create_note_tags_table()
    ];
    
    foreach ($tables as $sql) {
        dbDelta($sql);
    }
    
    // 添加默认数据
    $this->add_default_data();
}

private function create_notes_table() {
    global $wpdb;
    
    $table_name = $wpdb->prefix . 'md_notes';
    
    $sql = "CREATE TABLE IF NOT EXISTS $table_name (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        title VARCHAR(255) NOT NULL DEFAULT '',
        content LONGTEXT NOT NULL,
        excerpt TEXT,
        folder_id BIGINT(20) UNSIGNED DEFAULT 0,
        author_id BIGINT(20) UNSIGNED NOT NULL,
        status VARCHAR(20) NOT NULL DEFAULT 'draft',
        is_public TINYINT(1) NOT NULL DEFAULT 0,
        share_token VARCHAR(32),
        view_count BIGINT(20) UNSIGNED DEFAULT 0,
        last_modified_by BIGINT(20) UNSIGNED,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        KEY author_id (author_id),
        KEY folder_id (folder_id),
        KEY status (status),
        KEY share_token (share_token),
        KEY created_at (created_at),
        FULLTEXT KEY content_ft (title, content, excerpt)
    ) {$this->charset_collate};";
    
    return $sql;
}

private function create_note_versions_table() {
    global $wpdb;
    
    $table_name = $wpdb->prefix . 'md_note_versions';
    
    $sql = "CREATE TABLE IF NOT EXISTS $table_name (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        note_id BIGINT(20) UNSIGNED NOT NULL,
        version_number INT(11) NOT NULL,
        title VARCHAR(255),
        content LONGTEXT,
        author_id BIGINT(20) UNSIGNED NOT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        change_summary VARCHAR(255),
        PRIMARY KEY (id),
        KEY note_id (note_id),
        KEY version_number (version_number),
        KEY created_at (created_at),
        FOREIGN KEY (note_id) REFERENCES {$wpdb->prefix}md_notes(id) ON DELETE CASCADE
    ) {$this->charset_collate};";
    
    return $sql;
}

private function create_collaborators_table() {
    global $wpdb;
    
    $table_name = $wpdb->prefix . 'md_collaborators';
    
    $sql = "CREATE TABLE IF NOT EXISTS $table_name (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        note_id BIGINT(20) UNSIGNED NOT NULL,
        user_id BIGINT(20) UNSIGNED NOT NULL,
        permission_level ENUM('view', 'edit', 'admin') NOT NULL DEFAULT 'view',
        invited_by BIGINT(20) UNSIGNED,
        invited_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        last_accessed DATETIME,
        PRIMARY KEY (id),
        UNIQUE KEY note_user (note_id, user_id),
        KEY user_id (user_id),
        KEY permission_level (permission_level),
        FOREIGN KEY (note_id) REFERENCES {$wpdb->prefix}md_notes(id) ON DELETE CASCADE
    ) {$this->charset_collate};";
    
    return $sql;
}

private function create_folders_table() {
    global $wpdb;
    
    $table_name = $wpdb->prefix . 'md_folders';
    
    $sql = "CREATE TABLE IF NOT EXISTS $table_name (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        name VARCHAR(255) NOT NULL,
        parent_id BIGINT(20) UNSIGNED DEFAULT 0,
        author_id BIGINT(20) UNSIGNED NOT NULL,
        color VARCHAR(7),
        icon VARCHAR(50),
        sort_order INT(11) DEFAULT 0,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        KEY parent_id (parent_id),
        KEY author_id (author_id),
        KEY sort_order (sort_order)
    ) {$this->charset_collate};";
    
    return $sql;
}

private function create_tags_table() {
    global $wpdb;
    
    $table_name = $wpdb->prefix . 'md_tags';
    
    $sql = "CREATE TABLE IF NOT EXISTS $table_name (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        name VARCHAR(100) NOT NULL,
        slug VARCHAR(100) NOT NULL,
        author_id BIGINT(20) UNSIGNED NOT NULL,
        color VARCHAR(7),
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        UNIQUE KEY slug (slug),
        KEY author_id (author_id),
        KEY name (name)
    ) {$this->charset_collate};";
    
    return $sql;
}

private function create_note_tags_table() {
    global $wpdb;
    
    $table_name = $wpdb->prefix . 'md_note_tags';
    
    $sql = "CREATE TABLE IF NOT EXISTS $table_name (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        note_id BIGINT(20) UNSIGNED NOT NULL,
        tag_id BIGINT(20) UNSIGNED NOT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        UNIQUE KEY note_tag (note_id, tag_id),
        KEY tag_id (tag_id),
        FOREIGN KEY (note_id) REFERENCES {$wpdb->prefix}md_notes(id) ON DELETE CASCADE,
        FOREIGN KEY (tag_id) REFERENCES {$wpdb->prefix}md_tags(id) ON DELETE CASCADE
    ) {$this->charset_collate};";
    
    return $sql;
}

private function add_default_data() {
    global $wpdb;
    
    // 创建默认文件夹
    $user_id = get_current_user_id();
    if ($user_id) {
        $folders_table = $wpdb->prefix . 'md_folders';
        
        $default_folders = [
            ['name' => '个人笔记', 'color' => '#4ECDC4', 'icon' => '📝'],
            ['name' =>
本文来自网络,不代表柔性供应链服务中心立场,转载请注明出处:https://mall.org.cn/5272.html

EXCHANGES®作者

上一篇
下一篇

为您推荐

发表回复

联系我们

联系我们

18559313275

在线咨询: QQ交谈

邮箱: vip@exchanges.center

工作时间:周一至周五,9:00-17:30,节假日休息
返回顶部