文章目录[隐藏]
一步步实现:为WordPress打造内嵌的在线Markdown笔记与共享协作工具
引言:为什么WordPress需要内嵌Markdown协作工具?
在当今数字化工作环境中,内容创作和团队协作的效率直接影响着项目的成功。WordPress作为全球最流行的内容管理系统,虽然拥有强大的发布功能,但在实时协作和结构化笔记方面却存在明显不足。传统的WordPress编辑器对于需要频繁协作的技术团队、内容创作者和教育机构来说,往往显得笨重且效率低下。
Markdown作为一种轻量级标记语言,以其简洁的语法和清晰的格式呈现,已经成为技术文档、笔记和协作内容的首选格式。将Markdown编辑与实时协作功能集成到WordPress中,不仅可以提升内容创作效率,还能为团队提供无缝的协作体验。
本文将详细介绍如何通过WordPress代码二次开发,构建一个功能完整的内嵌式Markdown笔记与共享协作工具,让您的WordPress网站具备类似Notion、语雀等现代协作平台的核心功能。
第一部分:项目规划与技术选型
1.1 功能需求分析
在开始开发之前,我们需要明确工具的核心功能需求:
- Markdown编辑器:支持实时预览、语法高亮、常用格式快捷键
- 实时协作功能:多用户同时编辑、光标位置显示、更改实时同步
- 笔记管理:文件夹/标签分类、全文搜索、版本历史
- 权限控制系统:基于角色的访问控制、分享链接设置
- 数据存储与同步:可靠的数据存储机制、离线编辑支持
- 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: '', 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' =>
