文章目录
-
- 在当今数字化工作环境中,远程协作已成为常态。无论是产品设计、项目规划还是创意头脑风暴,团队成员往往分散在不同地点。传统的沟通方式如邮件、即时消息和视频会议虽然有效,但在创意协作方面存在明显局限——缺乏直观的视觉共享空间。 在线协同白板正是解决这一痛点的理想工具。它提供了一个虚拟的"画布",团队成员可以实时绘制图表、添加便签、上传图片、创建思维导图,并看到彼此的修改。这种视觉化协作方式能显著提高团队效率,激发创意灵感,并确保所有参与者对项目有统一的理解。 对于WordPress网站所有者来说,添加这样的功能不仅能提升用户体验,还能将你的网站从一个单向信息发布平台转变为互动协作空间。无论是企业内部协作、在线教育、咨询服务还是客户项目沟通,协同白板都能为你的网站增加巨大价值。
-
- 在开始之前,我们需要明确WordPress二次开发的基本概念。WordPress不仅是一个内容管理系统,更是一个强大的开发平台,通过其丰富的API和钩子系统,我们可以扩展其功能而不影响核心文件。 关键概念: 主题与插件:功能扩展主要通过子主题或自定义插件实现 动作钩子(Action Hooks):在特定时间点执行自定义代码 过滤器钩子(Filter Hooks):修改WordPress处理的数据 短代码(Shortcodes):在内容中嵌入动态功能 REST API:为前端应用提供数据接口
- 为了安全地进行开发,我们首先需要搭建本地测试环境: 本地服务器环境:推荐使用XAMPP、MAMP或Local by Flywheel 代码编辑器:VS Code、PHPStorm或Sublime Text 浏览器开发者工具:Chrome DevTools或Firefox Developer Tools 版本控制:Git用于代码管理
- 我们将创建一个独立的插件来管理所有协同白板功能,这样可以确保功能独立于主题,便于维护和迁移。 <?php /** * Plugin Name: 协同白板与团队协作工具 * Plugin URI: https://yourwebsite.com/ * Description: 为WordPress网站添加在线协同白板与团队创意协作功能 * Version: 1.0.0 * Author: 你的名字 * License: GPL v2 or later */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('COLLAB_WHITEBOARD_VERSION', '1.0.0'); define('COLLAB_WHITEBOARD_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('COLLAB_WHITEBOARD_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 function collab_whiteboard_init() { // 检查依赖项 if (!class_exists('WP_List_Table')) { require_once(ABSPATH . 'wp-admin/includes/class-wp-list-table.php'); } // 加载必要文件 require_once COLLAB_WHITEBOARD_PLUGIN_DIR . 'includes/class-whiteboard-manager.php'; require_once COLLAB_WHITEBOARD_PLUGIN_DIR . 'includes/class-whiteboard-db.php'; require_once COLLAB_WHITEBOARD_PLUGIN_DIR . 'includes/class-whiteboard-shortcodes.php'; // 初始化组件 new Whiteboard_Manager(); new Whiteboard_Shortcodes(); } add_action('plugins_loaded', 'collab_whiteboard_init'); // 激活插件时创建数据库表 function collab_whiteboard_activate() { require_once COLLAB_WHITEBOARD_PLUGIN_DIR . 'includes/class-whiteboard-db.php'; Whiteboard_DB::create_tables(); } register_activation_hook(__FILE__, 'collab_whiteboard_activate'); // 停用插件时的清理工作 function collab_whiteboard_deactivate() { // 可选的清理代码 } register_deactivation_hook(__FILE__, 'collab_whiteboard_deactivate');
-
- 协同白板需要存储白板数据、用户权限和修改历史。我们创建以下数据库表: // 在includes/class-whiteboard-db.php中 class Whiteboard_DB { public static function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_prefix = $wpdb->prefix . 'collab_'; // 白板主表 $whiteboards_table = $table_prefix . 'whiteboards'; $sql1 = "CREATE TABLE IF NOT EXISTS $whiteboards_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, title varchar(255) NOT NULL, description text, content longtext, created_by bigint(20) NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, settings text, is_public tinyint(1) DEFAULT 0, PRIMARY KEY (id) ) $charset_collate;"; // 白板权限表 $permissions_table = $table_prefix . 'permissions'; $sql2 = "CREATE TABLE IF NOT EXISTS $permissions_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, whiteboard_id mediumint(9) NOT NULL, user_id bigint(20) NOT NULL, permission_level varchar(50) NOT NULL, added_by bigint(20) NOT NULL, added_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY unique_whiteboard_user (whiteboard_id, user_id) ) $charset_collate;"; // 白板历史记录表 $history_table = $table_prefix . 'history'; $sql3 = "CREATE TABLE IF NOT EXISTS $history_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, whiteboard_id mediumint(9) NOT NULL, user_id bigint(20) NOT NULL, action varchar(100) NOT NULL, changes text, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY whiteboard_id (whiteboard_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql1); dbDelta($sql2); dbDelta($sql3); } }
- 协同白板需要精细的权限控制,不同用户应有不同级别的访问和编辑权限: class Whiteboard_Permissions { const PERMISSION_VIEW = 'view'; const PERMISSION_COMMENT = 'comment'; const PERMISSION_EDIT = 'edit'; const PERMISSION_ADMIN = 'admin'; /** * 检查用户对白板的权限 */ public static function check_permission($whiteboard_id, $user_id, $required_permission) { global $wpdb; // 获取白板信息 $table_name = $wpdb->prefix . 'collab_whiteboards'; $whiteboard = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $whiteboard_id )); if (!$whiteboard) { return false; } // 如果是公开白板且只需要查看权限 if ($whiteboard->is_public && $required_permission === self::PERMISSION_VIEW) { return true; } // 创建者拥有所有权限 if ($whiteboard->created_by == $user_id) { return true; } // 检查用户特定权限 $permissions_table = $wpdb->prefix . 'collab_permissions'; $user_permission = $wpdb->get_var($wpdb->prepare( "SELECT permission_level FROM $permissions_table WHERE whiteboard_id = %d AND user_id = %d", $whiteboard_id, $user_id )); if (!$user_permission) { return false; } // 权限等级映射 $permission_hierarchy = [ self::PERMISSION_VIEW => 1, self::PERMISSION_COMMENT => 2, self::PERMISSION_EDIT => 3, self::PERMISSION_ADMIN => 4 ]; $required_level = $permission_hierarchy[$required_permission] ?? 0; $user_level = $permission_hierarchy[$user_permission] ?? 0; return $user_level >= $required_level; } /** * 为用户分配白板权限 */ public static function assign_permission($whiteboard_id, $user_id, $permission_level, $assigned_by) { global $wpdb; $table_name = $wpdb->prefix . 'collab_permissions'; // 检查是否已存在权限记录 $existing = $wpdb->get_var($wpdb->prepare( "SELECT id FROM $table_name WHERE whiteboard_id = %d AND user_id = %d", $whiteboard_id, $user_id )); if ($existing) { // 更新现有权限 return $wpdb->update( $table_name, [ 'permission_level' => $permission_level, 'added_by' => $assigned_by ], [ 'whiteboard_id' => $whiteboard_id, 'user_id' => $user_id ] ); } else { // 插入新权限记录 return $wpdb->insert( $table_name, [ 'whiteboard_id' => $whiteboard_id, 'user_id' => $user_id, 'permission_level' => $permission_level, 'added_by' => $assigned_by ] ); } } }
-
- 对于协同白板,我们需要一个强大的前端绘图库。这里我们选择Fabric.js,它是一个功能强大的Canvas库,支持丰富的图形操作和事件处理。 首先,在插件中注册必要的脚本和样式: class Whiteboard_Assets { public static function enqueue_frontend_assets() { // Fabric.js 绘图库 wp_enqueue_script( 'fabric-js', 'https://cdnjs.cloudflare.com/ajax/libs/fabric.js/4.5.0/fabric.min.js', [], '4.5.0', true ); // Socket.io 客户端 (用于实时通信) wp_enqueue_script( 'socket-io', 'https://cdn.socket.io/4.5.0/socket.io.min.js', [], '4.5.0', true ); // 自定义白板脚本 wp_enqueue_script( 'collab-whiteboard', COLLAB_WHITEBOARD_PLUGIN_URL . 'assets/js/whiteboard.js', ['jquery', 'fabric-js', 'socket-io'], COLLAB_WHITEBOARD_VERSION, true ); // 白板样式 wp_enqueue_style( 'collab-whiteboard-style', COLLAB_WHITEBOARD_PLUGIN_URL . 'assets/css/whiteboard.css', [], COLLAB_WHITEBOARD_VERSION ); // 本地化脚本,传递必要数据 wp_localize_script('collab-whiteboard', 'whiteboardConfig', [ 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('collab_whiteboard_nonce'), 'currentUserId' => get_current_user_id(), 'socketServer' => self::get_socket_server_url() ]); } private static function get_socket_server_url() { // 这里返回你的WebSocket服务器地址 // 可以是独立的Node.js服务器或通过WordPress REST API实现 return home_url('/wp-json/collab-whiteboard/v1/socket'); } } add_action('wp_enqueue_scripts', ['Whiteboard_Assets', 'enqueue_frontend_assets']);
- 接下来,我们创建白板的主要HTML结构和JavaScript逻辑: <!-- 在短代码输出的HTML中 --> <div class="whiteboard-container" data-whiteboard-id="<?php echo $whiteboard_id; ?>"> <div class="whiteboard-toolbar"> <div class="tool-group"> <button class="tool-btn" data-tool="select" title="选择工具"> <i class="icon-cursor"></i> </button> <button class="tool-btn active" data-tool="pencil" title="铅笔"> <i class="icon-pencil"></i> </button> <button class="tool-btn" data-tool="line" title="直线"> <i class="icon-line"></i> </button> <button class="tool-btn" data-tool="rectangle" title="矩形"> <i class="icon-square"></i> </button> <button class="tool-btn" data-tool="circle" title="圆形"> <i class="icon-circle"></i> </button> <button class="tool-btn" data-tool="text" title="文本"> <i class="icon-text"></i> </button> </div> <div class="tool-group"> <input type="color" class="color-picker" value="#000000" title="颜色"> <input type="range" class="brush-size" min="1" max="50" value="3" title="笔刷大小"> <button class="tool-btn" data-action="undo" title="撤销"> <i class="icon-undo"></i> </button> <button class="tool-btn" data-action="redo" title="重做"> <i class="icon-redo"></i> </button> <button class="tool-btn" data-action="clear" title="清空白板"> <i class="icon-trash"></i> </button> </div> <div class="tool-group user-list"> <span class="online-users">在线用户: <span class="user-count">1</span></span> </div> </div> <div class="whiteboard-canvas-container"> <canvas id="whiteboard-canvas"></canvas> </div> <div class="whiteboard-sidebar"> <div class="sidebar-section"> <h4>元素属性</h4> <div class="properties-panel"> <!-- 动态属性控件将在这里显示 --> </div> </div> <div class="sidebar-section"> <h4>聊天与评论</h4> <div class="chat-container"> <div class="chat-messages"></div> <div class="chat-input"> <input type="text" placeholder="输入消息..."> <button class="send-btn">发送</button> </div> </div> </div> </div> </div>
- 实时协作是协同白板的核心功能。我们使用WebSocket实现实时数据同步: // assets/js/whiteboard.js class CollaborativeWhiteboard { constructor(containerElement) { this.container = containerElement; this.whiteboardId = containerElement.dataset.whiteboardId; this.canvas = null; this.socket = null; this.currentTool = 'pencil'; this.isDrawing = false; this.lastPoint = null; this.history = []; this.historyIndex = -1; this.init(); } init() { // 初始化画布 this.canvas = new fabric.Canvas('whiteboard-canvas', { isDrawingMode: true, width: this.container.querySelector('.whiteboard-canvas-container').offsetWidth, height: 600, backgroundColor: '#ffffff' }); // 连接WebSocket服务器 this.connectSocket(); // 绑定事件 this.bindEvents(); // 加载现有白板数据 this.loadWhiteboardData(); } connectSocket() { // 连接到WebSocket服务器 this.socket = io(whiteboardConfig.socketServer, { query: { whiteboardId: this.whiteboardId, userId: whiteboardConfig.currentUserId } }); // 监听服务器消息 this.socket.on('connect', () => { console.log('已连接到白板服务器'); }); this.socket.on('drawing', (data) => { this.handleRemoteDrawing(data); }); this.socket.on('object:modified', (data) => { this.handleRemoteObjectModification(data); }); this.socket.on('user:joined', (data) => { this.updateOnlineUsers(data.users); }); this.socket.on('user:left', (data) => { this.updateOnlineUsers(data.users); }); this.socket.on('chat:message', (data) => { this.addChatMessage(data); }); } bindEvents() { // 工具按钮点击事件 this.container.querySelectorAll('.tool-btn[data-tool]').forEach(btn => { btn.addEventListener('click', (e) => { this.setTool(e.target.closest('.tool-btn').dataset.tool); }); }); // 动作按钮点击事件 this.container.querySelectorAll('.tool-btn[data-action]').forEach(btn => { btn.addEventListener('click', (e) => { const action = e.target.closest('.tool-btn').dataset.action; this.handleAction(action); }); }); // 颜色选择器 EventListener('change', (e) => { this.setColor(e.target.value); }); // 笔刷大小 this.container.querySelector('.brush-size').addEventListener('input', (e) => { this.setBrushSize(parseInt(e.target.value)); }); // 画布事件 this.canvas.on('mouse:down', (options) => { this.onMouseDown(options); }); this.canvas.on('mouse:move', (options) => { this.onMouseMove(options); }); this.canvas.on('mouse:up', (options) => { this.onMouseUp(options); }); this.canvas.on('object:added', (options) => { this.onObjectAdded(options); }); this.canvas.on('object:modified', (options) => { this.onObjectModified(options); }); // 聊天功能 const chatInput = this.container.querySelector('.chat-input input'); const sendBtn = this.container.querySelector('.chat-input .send-btn'); sendBtn.addEventListener('click', () => { this.sendChatMessage(chatInput.value); chatInput.value = ''; }); chatInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { this.sendChatMessage(chatInput.value); chatInput.value = ''; } }); // 窗口大小调整 window.addEventListener('resize', () => { this.resizeCanvas(); }); } setTool(tool) { this.currentTool = tool; // 更新按钮状态 this.container.querySelectorAll('.tool-btn[data-tool]').forEach(btn => { btn.classList.toggle('active', btn.dataset.tool === tool); }); // 根据工具设置画布模式 switch(tool) { case 'select': this.canvas.isDrawingMode = false; this.canvas.selection = true; break; case 'pencil': this.canvas.isDrawingMode = true; this.canvas.freeDrawingBrush = new fabric.PencilBrush(this.canvas); this.canvas.freeDrawingBrush.width = this.brushSize || 3; this.canvas.freeDrawingBrush.color = this.currentColor || '#000000'; break; case 'line': this.canvas.isDrawingMode = false; this.canvas.selection = false; // 实现直线绘制逻辑 break; // 其他工具的实现... } } setColor(color) { this.currentColor = color; if (this.canvas.isDrawingMode) { this.canvas.freeDrawingBrush.color = color; } // 如果选择了对象,则更改对象颜色 const activeObject = this.canvas.getActiveObject(); if (activeObject) { activeObject.set('fill', color); this.canvas.renderAll(); this.sendObjectUpdate(activeObject); } } setBrushSize(size) { this.brushSize = size; if (this.canvas.isDrawingMode) { this.canvas.freeDrawingBrush.width = size; } } onMouseDown(options) { if (!options.target && this.currentTool === 'line') { this.isDrawing = true; this.lastPoint = options.pointer; } } onMouseMove(options) { if (this.isDrawing && this.currentTool === 'line') { // 绘制直线预览 } } onMouseUp(options) { if (this.isDrawing && this.currentTool === 'line' && this.lastPoint) { const line = new fabric.Line([ this.lastPoint.x, this.lastPoint.y, options.pointer.x, options.pointer.y ], { stroke: this.currentColor || '#000000', strokeWidth: this.brushSize || 3 }); this.canvas.add(line); this.isDrawing = false; this.lastPoint = null; } } onObjectAdded(options) { // 保存到历史记录 this.saveToHistory(); // 发送到服务器 if (options.target) { this.sendDrawingData({ type: 'object:added', object: options.target.toJSON(), userId: whiteboardConfig.currentUserId }); } } onObjectModified(options) { // 发送对象更新到服务器 if (options.target) { this.sendObjectUpdate(options.target); } } sendDrawingData(data) { if (this.socket && this.socket.connected) { this.socket.emit('drawing', { whiteboardId: this.whiteboardId, ...data }); } } sendObjectUpdate(object) { this.sendDrawingData({ type: 'object:modified', object: object.toJSON(), userId: whiteboardConfig.currentUserId }); } handleRemoteDrawing(data) { // 忽略自己发送的数据 if (data.userId === whiteboardConfig.currentUserId) return; switch(data.type) { case 'object:added': fabric.util.enlivenObjects([data.object], (objects) => { objects.forEach(obj => { this.canvas.add(obj); }); }); break; case 'object:modified': const object = this.canvas.getObjects().find(obj => obj.data && obj.data.id === data.object.data.id ); if (object) { object.set(data.object); this.canvas.renderAll(); } break; } } sendChatMessage(message) { if (!message.trim()) return; if (this.socket && this.socket.connected) { this.socket.emit('chat:message', { whiteboardId: this.whiteboardId, userId: whiteboardConfig.currentUserId, message: message, timestamp: new Date().toISOString() }); } } addChatMessage(data) { const chatMessages = this.container.querySelector('.chat-messages'); const messageElement = document.createElement('div'); messageElement.className = 'chat-message'; messageElement.innerHTML = ` <div class="message-header"> <span class="user-name">用户 ${data.userId}</span> <span class="message-time">${new Date(data.timestamp).toLocaleTimeString()}</span> </div> <div class="message-content">${this.escapeHtml(data.message)}</div> `; chatMessages.appendChild(messageElement); chatMessages.scrollTop = chatMessages.scrollHeight; } updateOnlineUsers(users) { const userCountElement = this.container.querySelector('.user-count'); if (userCountElement) { userCountElement.textContent = users.length; } } saveToHistory() { // 保存当前画布状态到历史记录 const state = JSON.stringify(this.canvas.toJSON()); this.history = this.history.slice(0, this.historyIndex + 1); this.history.push(state); this.historyIndex++; } loadWhiteboardData() { // 通过AJAX加载白板数据 jQuery.ajax({ url: whiteboardConfig.ajaxUrl, method: 'POST', data: { action: 'load_whiteboard', whiteboard_id: this.whiteboardId, nonce: whiteboardConfig.nonce }, success: (response) => { if (response.success && response.data) { this.canvas.loadFromJSON(response.data, () => { this.canvas.renderAll(); }); } } }); } resizeCanvas() { const container = this.container.querySelector('.whiteboard-canvas-container'); this.canvas.setDimensions({ width: container.offsetWidth, height: 600 }); } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } } // 初始化白板document.addEventListener('DOMContentLoaded', () => { const whiteboardContainers = document.querySelectorAll('.whiteboard-container'); whiteboardContainers.forEach(container => { new CollaborativeWhiteboard(container); }); }); ## 第四部分:后端WebSocket服务器与数据同步 ### 4.1 设置WebSocket服务器 对于实时协作,我们需要一个WebSocket服务器来处理客户端连接和数据广播。这里我们使用Node.js和Socket.io: // server/whiteboard-server.jsconst http = require('http');const socketIo = require('socket.io');const mysql = require('mysql2/promise'); // 创建HTTP服务器const server = http.createServer();const io = socketIo(server, { cors: { origin: process.env.WORDPRESS_URL || "http://localhost", methods: ["GET", "POST"] } }); // 数据库连接池const dbPool = mysql.createPool({ host: process.env.DB_HOST || 'localhost', user: process.env.DB_USER || 'wordpress_user', password: process.env.DB_PASSWORD || 'password', database: process.env.DB_NAME || 'wordpress_db', waitForConnections: true, connectionLimit: 10, queueLimit: 0 }); // 存储在线用户const onlineUsers = new Map(); // whiteboardId -> Set of userIdsconst userSockets = new Map(); // userId -> Set of socketIds io.on('connection', (socket) => { const { whiteboardId, userId } = socket.handshake.query; console.log(`用户 ${userId} 连接到白板 ${whiteboardId}`); // 验证用户权限 validateUserPermission(whiteboardId, userId).then(hasPermission => { if (!hasPermission) { socket.disconnect(); return; } // 加入白板房间 socket.join(`whiteboard:${whiteboardId}`); // 更新在线用户列表 if (!onlineUsers.has(whiteboardId)) { onlineUsers.set(whiteboardId, new Set()); } onlineUsers.get(whiteboardId).add(userId); // 存储用户socket映射 if (!userSockets.has(userId)) { userSockets.set(userId, new Set()); } userSockets.get(userId).add(socket.id); // 广播用户加入事件 io.to(`whiteboard:${whiteboardId}`).emit('user:joined', { whiteboardId, userId, users: Array.from(onlineUsers.get(whiteboardId)) }); // 处理绘图事件 socket.on('drawing', (data) => { // 验证数据 if (data.whiteboardId !== whiteboardId) return; // 广播给同一白板的其他用户 socket.to(`whiteboard:${whiteboardId}`).emit('drawing', { ...data, timestamp: new Date().toISOString() }); // 保存到数据库(可选,根据需求) saveDrawingAction(whiteboardId, userId, data); }); // 处理聊天消息 socket.on('chat:message', (data) => { if (data.whiteboardId !== whiteboardId) return; // 广播聊天消息 io.to(`whiteboard:${whiteboardId}`).emit('chat:message', { ...data, timestamp: new Date().toISOString() }); // 保存聊天记录到数据库 saveChatMessage(whiteboardId, userId, data.message); }); // 处理断开连接 socket.on('disconnect', () => { console.log(`用户 ${userId} 断开连接`); // 清理用户socket映射 if (userSockets.has(userId)) { userSockets.get(userId).delete(socket.id); if (userSockets.get(userId).size === 0) { userSockets.delete(userId); // 从在线用户中移除 if (onlineUsers.has(whiteboardId)) { onlineUsers.get(whiteboardId).delete(userId); // 广播用户离开事件 io.to(`whiteboard:${whiteboardId}`).emit('user:left', { whiteboardId, userId, users: Array.from(onlineUsers.get(whiteboardId)) }); } } } }); }).catch(error => { console.error('权限验证失败:', error); socket.disconnect(); }); }); // 验证用户权限async function validateUserPermission(whiteboardId, userId) { try { const [rows] = await dbPool.execute( `SELECT w.is_public, p.permission_level FROM wp_collab_whiteboards w LEFT JOIN wp_collab_permissions p ON w.id = p.whiteboard_id AND p.user_id = ? WHERE w.id = ?`, [userId, whiteboardId] ); if (rows.length === 0) return false; const whiteboard = rows[0]; // 检查权限 if (whiteboard.is_public) return true; if (whiteboard.permission_level) return true; return false; } catch (error) { console.error('数据库查询错误:', error); return false; } } // 保存绘图动作到历史记录async function saveDrawingAction(whiteboardId, userId, data) { try { await dbPool.execute( `INSERT INTO wp_collab_history (whiteboard_id, user_id, action, changes) VALUES (?, ?, ?, ?)`, [whiteboardId, userId, data.type, JSON.stringify(data)] ); } catch (error) { console.error('保存历史记录失败:', error); } } // 保存聊天消息async function saveChatMessage(whiteboardId, userId, message) { try { await dbPool.execute( `INSERT INTO wp_collab_chat (whiteboard_id, user_id, message) VALUES (?, ?, ?)`, [whiteboardId, userId, message] ); } catch (error) { console.error('保存聊天消息失败:', error); } } // 启动服务器const PORT = process.env.PORT || 3000;server.listen(PORT, () => { console.log(`白板服务器运行在端口 ${PORT}`); }); ### 4.2 WordPress REST API集成 为了让WebSocket服务器与WordPress通信,我们需要创建REST API端点: // includes/class-whiteboard-api.phpclass Whiteboard_API { public function register_routes() { register_rest_route('collab-whiteboard/v1', '/whiteboard/(?P<id>d+)', [ [ 'methods' => 'GET', 'callback' => [$this, 'get_whiteboard'], 'permission_callback' => [$this, 'check_whiteboard_permission'] ], [ 'methods' => 'POST', 'callback' => [$this, 'update_whiteboard'], 'permission_callback' => [$this, 'check_edit_permission'] ] ]); register_rest_route('collab-whiteboard/v1', '/whiteboard/(?P<id>d+)/history', [ [ 'methods' => 'GET', 'callback' => [$this, 'get_whiteboard_history'], 'permission_callback' => [$this, 'check_whiteboard_permission'] ] ]); register_rest_route('collab-whiteboard/v1', '/whiteboard/(?P<id>d+)/chat', [ [ 'methods' => 'GET', 'callback' => [$this, 'get_chat_messages'], 'permission_callback' => [$this, 'check_whiteboard_permission'] ], [ 'methods' => 'POST', 'callback' => [$this, 'post_chat_message'], 'permission_callback' => [$this, 'check_comment_permission'] ] ]); } public function get_whiteboard($request) { $whiteboard_id = $request['id']; global $wpdb; $table_name = $wpdb->prefix . 'collab_whiteboards'; $whiteboard = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $whiteboard_id )); if (!$whiteboard) { return new WP_Error('not_found', '白板不存在', ['status' => 404]); } return rest_ensure_response([ 'id' => $whiteboard->id, 'title' => $whiteboard->title, 'description' => $whiteboard->description, 'content' => json_decode($whiteboard->content, true), 'settings' => json_decode($whiteboard->settings, true), 'created_by' => $whiteboard->created_by, 'created_at' => $whiteboard->created_at, 'updated_at' => $whiteboard->updated_at, 'is_public' => (bool)$whiteboard->is_public ]); } public function update_whiteboard($request) { $whiteboard_id = $request['id']; $content = $request->get_param('content'); if (empty($content)) { return new WP_Error('invalid_data', '内容不能为空', ['status' => 400]); } global $wpdb; $table_name = $wpdb->prefix . 'collab_whiteboards'; $result = $wpdb->update( $table_name, [ 'content' => json_encode($content), 'updated_at' => current_time('mysql') ], ['id' => $whiteboard_id] ); if ($result === false) { return new WP_Error('update_failed', '更新失败', ['status' => 500]); } return rest_ensure_response([ 'success' => true, 'message' => '白板已更新' ]); } public function check_whiteboard_permission($request) { $whiteboard_id = $request['id']; $user_id = get_current_user_id(); return Whiteboard_Permissions::check_permission( $whiteboard_id, $user_id, Whiteboard_Permissions::PERMISSION_VIEW
在当今数字化工作环境中,远程协作已成为常态。无论是产品设计、项目规划还是创意头脑风暴,团队成员往往分散在不同地点。传统的沟通方式如邮件、即时消息和视频会议虽然有效,但在创意协作方面存在明显局限——缺乏直观的视觉共享空间。
在线协同白板正是解决这一痛点的理想工具。它提供了一个虚拟的"画布",团队成员可以实时绘制图表、添加便签、上传图片、创建思维导图,并看到彼此的修改。这种视觉化协作方式能显著提高团队效率,激发创意灵感,并确保所有参与者对项目有统一的理解。
对于WordPress网站所有者来说,添加这样的功能不仅能提升用户体验,还能将你的网站从一个单向信息发布平台转变为互动协作空间。无论是企业内部协作、在线教育、咨询服务还是客户项目沟通,协同白板都能为你的网站增加巨大价值。
在开始之前,我们需要明确WordPress二次开发的基本概念。WordPress不仅是一个内容管理系统,更是一个强大的开发平台,通过其丰富的API和钩子系统,我们可以扩展其功能而不影响核心文件。
关键概念:
- 主题与插件:功能扩展主要通过子主题或自定义插件实现
- 动作钩子(Action Hooks):在特定时间点执行自定义代码
- 过滤器钩子(Filter Hooks):修改WordPress处理的数据
- 短代码(Shortcodes):在内容中嵌入动态功能
- REST API:为前端应用提供数据接口
为了安全地进行开发,我们首先需要搭建本地测试环境:
- 本地服务器环境:推荐使用XAMPP、MAMP或Local by Flywheel
- 代码编辑器:VS Code、PHPStorm或Sublime Text
- 浏览器开发者工具:Chrome DevTools或Firefox Developer Tools
- 版本控制:Git用于代码管理
我们将创建一个独立的插件来管理所有协同白板功能,这样可以确保功能独立于主题,便于维护和迁移。
<?php
/**
* Plugin Name: 协同白板与团队协作工具
* Plugin URI: https://yourwebsite.com/
* Description: 为WordPress网站添加在线协同白板与团队创意协作功能
* Version: 1.0.0
* Author: 你的名字
* License: GPL v2 or later
*/
// 防止直接访问
if (!defined('ABSPATH')) {
exit;
}
// 定义插件常量
define('COLLAB_WHITEBOARD_VERSION', '1.0.0');
define('COLLAB_WHITEBOARD_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('COLLAB_WHITEBOARD_PLUGIN_URL', plugin_dir_url(__FILE__));
// 初始化插件
function collab_whiteboard_init() {
// 检查依赖项
if (!class_exists('WP_List_Table')) {
require_once(ABSPATH . 'wp-admin/includes/class-wp-list-table.php');
}
// 加载必要文件
require_once COLLAB_WHITEBOARD_PLUGIN_DIR . 'includes/class-whiteboard-manager.php';
require_once COLLAB_WHITEBOARD_PLUGIN_DIR . 'includes/class-whiteboard-db.php';
require_once COLLAB_WHITEBOARD_PLUGIN_DIR . 'includes/class-whiteboard-shortcodes.php';
// 初始化组件
new Whiteboard_Manager();
new Whiteboard_Shortcodes();
}
add_action('plugins_loaded', 'collab_whiteboard_init');
// 激活插件时创建数据库表
function collab_whiteboard_activate() {
require_once COLLAB_WHITEBOARD_PLUGIN_DIR . 'includes/class-whiteboard-db.php';
Whiteboard_DB::create_tables();
}
register_activation_hook(__FILE__, 'collab_whiteboard_activate');
// 停用插件时的清理工作
function collab_whiteboard_deactivate() {
// 可选的清理代码
}
register_deactivation_hook(__FILE__, 'collab_whiteboard_deactivate');
协同白板需要存储白板数据、用户权限和修改历史。我们创建以下数据库表:
// 在includes/class-whiteboard-db.php中
class Whiteboard_DB {
public static function create_tables() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$table_prefix = $wpdb->prefix . 'collab_';
// 白板主表
$whiteboards_table = $table_prefix . 'whiteboards';
$sql1 = "CREATE TABLE IF NOT EXISTS $whiteboards_table (
id mediumint(9) NOT NULL AUTO_INCREMENT,
title varchar(255) NOT NULL,
description text,
content longtext,
created_by bigint(20) NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
settings text,
is_public tinyint(1) DEFAULT 0,
PRIMARY KEY (id)
) $charset_collate;";
// 白板权限表
$permissions_table = $table_prefix . 'permissions';
$sql2 = "CREATE TABLE IF NOT EXISTS $permissions_table (
id mediumint(9) NOT NULL AUTO_INCREMENT,
whiteboard_id mediumint(9) NOT NULL,
user_id bigint(20) NOT NULL,
permission_level varchar(50) NOT NULL,
added_by bigint(20) NOT NULL,
added_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY unique_whiteboard_user (whiteboard_id, user_id)
) $charset_collate;";
// 白板历史记录表
$history_table = $table_prefix . 'history';
$sql3 = "CREATE TABLE IF NOT EXISTS $history_table (
id mediumint(9) NOT NULL AUTO_INCREMENT,
whiteboard_id mediumint(9) NOT NULL,
user_id bigint(20) NOT NULL,
action varchar(100) NOT NULL,
changes text,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY whiteboard_id (whiteboard_id)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql1);
dbDelta($sql2);
dbDelta($sql3);
}
}
协同白板需要精细的权限控制,不同用户应有不同级别的访问和编辑权限:
class Whiteboard_Permissions {
const PERMISSION_VIEW = 'view';
const PERMISSION_COMMENT = 'comment';
const PERMISSION_EDIT = 'edit';
const PERMISSION_ADMIN = 'admin';
/**
* 检查用户对白板的权限
*/
public static function check_permission($whiteboard_id, $user_id, $required_permission) {
global $wpdb;
// 获取白板信息
$table_name = $wpdb->prefix . 'collab_whiteboards';
$whiteboard = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $table_name WHERE id = %d",
$whiteboard_id
));
if (!$whiteboard) {
return false;
}
// 如果是公开白板且只需要查看权限
if ($whiteboard->is_public && $required_permission === self::PERMISSION_VIEW) {
return true;
}
// 创建者拥有所有权限
if ($whiteboard->created_by == $user_id) {
return true;
}
// 检查用户特定权限
$permissions_table = $wpdb->prefix . 'collab_permissions';
$user_permission = $wpdb->get_var($wpdb->prepare(
"SELECT permission_level FROM $permissions_table
WHERE whiteboard_id = %d AND user_id = %d",
$whiteboard_id, $user_id
));
if (!$user_permission) {
return false;
}
// 权限等级映射
$permission_hierarchy = [
self::PERMISSION_VIEW => 1,
self::PERMISSION_COMMENT => 2,
self::PERMISSION_EDIT => 3,
self::PERMISSION_ADMIN => 4
];
$required_level = $permission_hierarchy[$required_permission] ?? 0;
$user_level = $permission_hierarchy[$user_permission] ?? 0;
return $user_level >= $required_level;
}
/**
* 为用户分配白板权限
*/
public static function assign_permission($whiteboard_id, $user_id, $permission_level, $assigned_by) {
global $wpdb;
$table_name = $wpdb->prefix . 'collab_permissions';
// 检查是否已存在权限记录
$existing = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM $table_name
WHERE whiteboard_id = %d AND user_id = %d",
$whiteboard_id, $user_id
));
if ($existing) {
// 更新现有权限
return $wpdb->update(
$table_name,
[
'permission_level' => $permission_level,
'added_by' => $assigned_by
],
[
'whiteboard_id' => $whiteboard_id,
'user_id' => $user_id
]
);
} else {
// 插入新权限记录
return $wpdb->insert(
$table_name,
[
'whiteboard_id' => $whiteboard_id,
'user_id' => $user_id,
'permission_level' => $permission_level,
'added_by' => $assigned_by
]
);
}
}
}
对于协同白板,我们需要一个强大的前端绘图库。这里我们选择Fabric.js,它是一个功能强大的Canvas库,支持丰富的图形操作和事件处理。
首先,在插件中注册必要的脚本和样式:
class Whiteboard_Assets {
public static function enqueue_frontend_assets() {
// Fabric.js 绘图库
wp_enqueue_script(
'fabric-js',
'https://cdnjs.cloudflare.com/ajax/libs/fabric.js/4.5.0/fabric.min.js',
[],
'4.5.0',
true
);
// Socket.io 客户端 (用于实时通信)
wp_enqueue_script(
'socket-io',
'https://cdn.socket.io/4.5.0/socket.io.min.js',
[],
'4.5.0',
true
);
// 自定义白板脚本
wp_enqueue_script(
'collab-whiteboard',
COLLAB_WHITEBOARD_PLUGIN_URL . 'assets/js/whiteboard.js',
['jquery', 'fabric-js', 'socket-io'],
COLLAB_WHITEBOARD_VERSION,
true
);
// 白板样式
wp_enqueue_style(
'collab-whiteboard-style',
COLLAB_WHITEBOARD_PLUGIN_URL . 'assets/css/whiteboard.css',
[],
COLLAB_WHITEBOARD_VERSION
);
// 本地化脚本,传递必要数据
wp_localize_script('collab-whiteboard', 'whiteboardConfig', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('collab_whiteboard_nonce'),
'currentUserId' => get_current_user_id(),
'socketServer' => self::get_socket_server_url()
]);
}
private static function get_socket_server_url() {
// 这里返回你的WebSocket服务器地址
// 可以是独立的Node.js服务器或通过WordPress REST API实现
return home_url('/wp-json/collab-whiteboard/v1/socket');
}
}
add_action('wp_enqueue_scripts', ['Whiteboard_Assets', 'enqueue_frontend_assets']);
接下来,我们创建白板的主要HTML结构和JavaScript逻辑:
<!-- 在短代码输出的HTML中 -->
<div class="whiteboard-container" data-whiteboard-id="<?php echo $whiteboard_id; ?>">
<div class="whiteboard-toolbar">
<div class="tool-group">
<button class="tool-btn" data-tool="select" title="选择工具">
<i class="icon-cursor"></i>
</button>
<button class="tool-btn active" data-tool="pencil" title="铅笔">
<i class="icon-pencil"></i>
</button>
<button class="tool-btn" data-tool="line" title="直线">
<i class="icon-line"></i>
</button>
<button class="tool-btn" data-tool="rectangle" title="矩形">
<i class="icon-square"></i>
</button>
<button class="tool-btn" data-tool="circle" title="圆形">
<i class="icon-circle"></i>
</button>
<button class="tool-btn" data-tool="text" title="文本">
<i class="icon-text"></i>
</button>
</div>
<div class="tool-group">
<input type="color" class="color-picker" value="#000000" title="颜色">
<input type="range" class="brush-size" min="1" max="50" value="3" title="笔刷大小">
<button class="tool-btn" data-action="undo" title="撤销">
<i class="icon-undo"></i>
</button>
<button class="tool-btn" data-action="redo" title="重做">
<i class="icon-redo"></i>
</button>
<button class="tool-btn" data-action="clear" title="清空白板">
<i class="icon-trash"></i>
</button>
</div>
<div class="tool-group user-list">
<span class="online-users">在线用户: <span class="user-count">1</span></span>
</div>
</div>
<div class="whiteboard-canvas-container">
<canvas id="whiteboard-canvas"></canvas>
</div>
<div class="whiteboard-sidebar">
<div class="sidebar-section">
<h4>元素属性</h4>
<div class="properties-panel">
<!-- 动态属性控件将在这里显示 -->
</div>
</div>
<div class="sidebar-section">
<h4>聊天与评论</h4>
<div class="chat-container">
<div class="chat-messages"></div>
<div class="chat-input">
<input type="text" placeholder="输入消息...">
<button class="send-btn">发送</button>
</div>
</div>
</div>
</div>
</div>
实时协作是协同白板的核心功能。我们使用WebSocket实现实时数据同步:
// assets/js/whiteboard.js
class CollaborativeWhiteboard {
constructor(containerElement) {
this.container = containerElement;
this.whiteboardId = containerElement.dataset.whiteboardId;
this.canvas = null;
this.socket = null;
this.currentTool = 'pencil';
this.isDrawing = false;
this.lastPoint = null;
this.history = [];
this.historyIndex = -1;
this.init();
}
init() {
// 初始化画布
this.canvas = new fabric.Canvas('whiteboard-canvas', {
isDrawingMode: true,
width: this.container.querySelector('.whiteboard-canvas-container').offsetWidth,
height: 600,
backgroundColor: '#ffffff'
});
// 连接WebSocket服务器
this.connectSocket();
// 绑定事件
this.bindEvents();
// 加载现有白板数据
this.loadWhiteboardData();
}
connectSocket() {
// 连接到WebSocket服务器
this.socket = io(whiteboardConfig.socketServer, {
query: {
whiteboardId: this.whiteboardId,
userId: whiteboardConfig.currentUserId
}
});
// 监听服务器消息
this.socket.on('connect', () => {
console.log('已连接到白板服务器');
});
this.socket.on('drawing', (data) => {
this.handleRemoteDrawing(data);
});
this.socket.on('object:modified', (data) => {
this.handleRemoteObjectModification(data);
});
this.socket.on('user:joined', (data) => {
this.updateOnlineUsers(data.users);
});
this.socket.on('user:left', (data) => {
this.updateOnlineUsers(data.users);
});
this.socket.on('chat:message', (data) => {
this.addChatMessage(data);
});
}
bindEvents() {
// 工具按钮点击事件
this.container.querySelectorAll('.tool-btn[data-tool]').forEach(btn => {
btn.addEventListener('click', (e) => {
this.setTool(e.target.closest('.tool-btn').dataset.tool);
});
});
// 动作按钮点击事件
this.container.querySelectorAll('.tool-btn[data-action]').forEach(btn => {
btn.addEventListener('click', (e) => {
const action = e.target.closest('.tool-btn').dataset.action;
this.handleAction(action);
});
});
// 颜色选择器
EventListener('change', (e) => {
this.setColor(e.target.value);
});
// 笔刷大小
this.container.querySelector('.brush-size').addEventListener('input', (e) => {
this.setBrushSize(parseInt(e.target.value));
});
// 画布事件
this.canvas.on('mouse:down', (options) => {
this.onMouseDown(options);
});
this.canvas.on('mouse:move', (options) => {
this.onMouseMove(options);
});
this.canvas.on('mouse:up', (options) => {
this.onMouseUp(options);
});
this.canvas.on('object:added', (options) => {
this.onObjectAdded(options);
});
this.canvas.on('object:modified', (options) => {
this.onObjectModified(options);
});
// 聊天功能
const chatInput = this.container.querySelector('.chat-input input');
const sendBtn = this.container.querySelector('.chat-input .send-btn');
sendBtn.addEventListener('click', () => {
this.sendChatMessage(chatInput.value);
chatInput.value = '';
});
chatInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.sendChatMessage(chatInput.value);
chatInput.value = '';
}
});
// 窗口大小调整
window.addEventListener('resize', () => {
this.resizeCanvas();
});
}
setTool(tool) {
this.currentTool = tool;
// 更新按钮状态
this.container.querySelectorAll('.tool-btn[data-tool]').forEach(btn => {
btn.classList.toggle('active', btn.dataset.tool === tool);
});
// 根据工具设置画布模式
switch(tool) {
case 'select':
this.canvas.isDrawingMode = false;
this.canvas.selection = true;
break;
case 'pencil':
this.canvas.isDrawingMode = true;
this.canvas.freeDrawingBrush = new fabric.PencilBrush(this.canvas);
this.canvas.freeDrawingBrush.width = this.brushSize || 3;
this.canvas.freeDrawingBrush.color = this.currentColor || '#000000';
break;
case 'line':
this.canvas.isDrawingMode = false;
this.canvas.selection = false;
// 实现直线绘制逻辑
break;
// 其他工具的实现...
}
}
setColor(color) {
this.currentColor = color;
if (this.canvas.isDrawingMode) {
this.canvas.freeDrawingBrush.color = color;
}
// 如果选择了对象,则更改对象颜色
const activeObject = this.canvas.getActiveObject();
if (activeObject) {
activeObject.set('fill', color);
this.canvas.renderAll();
this.sendObjectUpdate(activeObject);
}
}
setBrushSize(size) {
this.brushSize = size;
if (this.canvas.isDrawingMode) {
this.canvas.freeDrawingBrush.width = size;
}
}
onMouseDown(options) {
if (!options.target && this.currentTool === 'line') {
this.isDrawing = true;
this.lastPoint = options.pointer;
}
}
onMouseMove(options) {
if (this.isDrawing && this.currentTool === 'line') {
// 绘制直线预览
}
}
onMouseUp(options) {
if (this.isDrawing && this.currentTool === 'line' && this.lastPoint) {
const line = new fabric.Line([
this.lastPoint.x, this.lastPoint.y,
options.pointer.x, options.pointer.y
], {
stroke: this.currentColor || '#000000',
strokeWidth: this.brushSize || 3
});
this.canvas.add(line);
this.isDrawing = false;
this.lastPoint = null;
}
}
onObjectAdded(options) {
// 保存到历史记录
this.saveToHistory();
// 发送到服务器
if (options.target) {
this.sendDrawingData({
type: 'object:added',
object: options.target.toJSON(),
userId: whiteboardConfig.currentUserId
});
}
}
onObjectModified(options) {
// 发送对象更新到服务器
if (options.target) {
this.sendObjectUpdate(options.target);
}
}
sendDrawingData(data) {
if (this.socket && this.socket.connected) {
this.socket.emit('drawing', {
whiteboardId: this.whiteboardId,
...data
});
}
}
sendObjectUpdate(object) {
this.sendDrawingData({
type: 'object:modified',
object: object.toJSON(),
userId: whiteboardConfig.currentUserId
});
}
handleRemoteDrawing(data) {
// 忽略自己发送的数据
if (data.userId === whiteboardConfig.currentUserId) return;
switch(data.type) {
case 'object:added':
fabric.util.enlivenObjects([data.object], (objects) => {
objects.forEach(obj => {
this.canvas.add(obj);
});
});
break;
case 'object:modified':
const object = this.canvas.getObjects().find(obj =>
obj.data && obj.data.id === data.object.data.id
);
if (object) {
object.set(data.object);
this.canvas.renderAll();
}
break;
}
}
sendChatMessage(message) {
if (!message.trim()) return;
if (this.socket && this.socket.connected) {
this.socket.emit('chat:message', {
whiteboardId: this.whiteboardId,
userId: whiteboardConfig.currentUserId,
message: message,
timestamp: new Date().toISOString()
});
}
}
addChatMessage(data) {
const chatMessages = this.container.querySelector('.chat-messages');
const messageElement = document.createElement('div');
messageElement.className = 'chat-message';
messageElement.innerHTML = `
<div class="message-header">
<span class="user-name">用户 ${data.userId}</span>
<span class="message-time">${new Date(data.timestamp).toLocaleTimeString()}</span>
</div>
<div class="message-content">${this.escapeHtml(data.message)}</div>
`;
chatMessages.appendChild(messageElement);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
updateOnlineUsers(users) {
const userCountElement = this.container.querySelector('.user-count');
if (userCountElement) {
userCountElement.textContent = users.length;
}
}
saveToHistory() {
// 保存当前画布状态到历史记录
const state = JSON.stringify(this.canvas.toJSON());
this.history = this.history.slice(0, this.historyIndex + 1);
this.history.push(state);
this.historyIndex++;
}
loadWhiteboardData() {
// 通过AJAX加载白板数据
jQuery.ajax({
url: whiteboardConfig.ajaxUrl,
method: 'POST',
data: {
action: 'load_whiteboard',
whiteboard_id: this.whiteboardId,
nonce: whiteboardConfig.nonce
},
success: (response) => {
if (response.success && response.data) {
this.canvas.loadFromJSON(response.data, () => {
this.canvas.renderAll();
});
}
}
});
}
resizeCanvas() {
const container = this.container.querySelector('.whiteboard-canvas-container');
this.canvas.setDimensions({
width: container.offsetWidth,
height: 600
});
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
// 初始化白板
document.addEventListener('DOMContentLoaded', () => {
const whiteboardContainers = document.querySelectorAll('.whiteboard-container');
whiteboardContainers.forEach(container => {
new CollaborativeWhiteboard(container);
});
});
## 第四部分:后端WebSocket服务器与数据同步
### 4.1 设置WebSocket服务器
对于实时协作,我们需要一个WebSocket服务器来处理客户端连接和数据广播。这里我们使用Node.js和Socket.io:
// server/whiteboard-server.js
const http = require('http');
const socketIo = require('socket.io');
const mysql = require('mysql2/promise');
// 创建HTTP服务器
const server = http.createServer();
const io = socketIo(server, {
cors: {
origin: process.env.WORDPRESS_URL || "http://localhost",
methods: ["GET", "POST"]
}
});
// 数据库连接池
const dbPool = mysql.createPool({
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'wordpress_user',
password: process.env.DB_PASSWORD || 'password',
database: process.env.DB_NAME || 'wordpress_db',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
// 存储在线用户
const onlineUsers = new Map(); // whiteboardId -> Set of userIds
const userSockets = new Map(); // userId -> Set of socketIds
io.on('connection', (socket) => {
const { whiteboardId, userId } = socket.handshake.query;
console.log(`用户 ${userId} 连接到白板 ${whiteboardId}`);
// 验证用户权限
validateUserPermission(whiteboardId, userId).then(hasPermission => {
if (!hasPermission) {
socket.disconnect();
return;
}
// 加入白板房间
socket.join(`whiteboard:${whiteboardId}`);
// 更新在线用户列表
if (!onlineUsers.has(whiteboardId)) {
onlineUsers.set(whiteboardId, new Set());
}
onlineUsers.get(whiteboardId).add(userId);
// 存储用户socket映射
if (!userSockets.has(userId)) {
userSockets.set(userId, new Set());
}
userSockets.get(userId).add(socket.id);
// 广播用户加入事件
io.to(`whiteboard:${whiteboardId}`).emit('user:joined', {
whiteboardId,
userId,
users: Array.from(onlineUsers.get(whiteboardId))
});
// 处理绘图事件
socket.on('drawing', (data) => {
// 验证数据
if (data.whiteboardId !== whiteboardId) return;
// 广播给同一白板的其他用户
socket.to(`whiteboard:${whiteboardId}`).emit('drawing', {
...data,
timestamp: new Date().toISOString()
});
// 保存到数据库(可选,根据需求)
saveDrawingAction(whiteboardId, userId, data);
});
// 处理聊天消息
socket.on('chat:message', (data) => {
if (data.whiteboardId !== whiteboardId) return;
// 广播聊天消息
io.to(`whiteboard:${whiteboardId}`).emit('chat:message', {
...data,
timestamp: new Date().toISOString()
});
// 保存聊天记录到数据库
saveChatMessage(whiteboardId, userId, data.message);
});
// 处理断开连接
socket.on('disconnect', () => {
console.log(`用户 ${userId} 断开连接`);
// 清理用户socket映射
if (userSockets.has(userId)) {
userSockets.get(userId).delete(socket.id);
if (userSockets.get(userId).size === 0) {
userSockets.delete(userId);
// 从在线用户中移除
if (onlineUsers.has(whiteboardId)) {
onlineUsers.get(whiteboardId).delete(userId);
// 广播用户离开事件
io.to(`whiteboard:${whiteboardId}`).emit('user:left', {
whiteboardId,
userId,
users: Array.from(onlineUsers.get(whiteboardId))
});
}
}
}
});
}).catch(error => {
console.error('权限验证失败:', error);
socket.disconnect();
});
});
// 验证用户权限
async function validateUserPermission(whiteboardId, userId) {
try {
const [rows] = await dbPool.execute(
`SELECT w.is_public, p.permission_level
FROM wp_collab_whiteboards w
LEFT JOIN wp_collab_permissions p ON w.id = p.whiteboard_id AND p.user_id = ?
WHERE w.id = ?`,
[userId, whiteboardId]
);
if (rows.length === 0) return false;
const whiteboard = rows[0];
// 检查权限
if (whiteboard.is_public) return true;
if (whiteboard.permission_level) return true;
return false;
} catch (error) {
console.error('数据库查询错误:', error);
return false;
}
}
// 保存绘图动作到历史记录
async function saveDrawingAction(whiteboardId, userId, data) {
try {
await dbPool.execute(
`INSERT INTO wp_collab_history
(whiteboard_id, user_id, action, changes)
VALUES (?, ?, ?, ?)`,
[whiteboardId, userId, data.type, JSON.stringify(data)]
);
} catch (error) {
console.error('保存历史记录失败:', error);
}
}
// 保存聊天消息
async function saveChatMessage(whiteboardId, userId, message) {
try {
await dbPool.execute(
`INSERT INTO wp_collab_chat
(whiteboard_id, user_id, message)
VALUES (?, ?, ?)`,
[whiteboardId, userId, message]
);
} catch (error) {
console.error('保存聊天消息失败:', error);
}
}
// 启动服务器
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`白板服务器运行在端口 ${PORT}`);
});
### 4.2 WordPress REST API集成
为了让WebSocket服务器与WordPress通信,我们需要创建REST API端点:
// includes/class-whiteboard-api.php
class Whiteboard_API {
public function register_routes() {
register_rest_route('collab-whiteboard/v1', '/whiteboard/(?P<id>d+)', [
[
'methods' => 'GET',
'callback' => [$this, 'get_whiteboard'],
'permission_callback' => [$this, 'check_whiteboard_permission']
],
[
'methods' => 'POST',
'callback' => [$this, 'update_whiteboard'],
'permission_callback' => [$this, 'check_edit_permission']
]
]);
register_rest_route('collab-whiteboard/v1', '/whiteboard/(?P<id>d+)/history', [
[
'methods' => 'GET',
'callback' => [$this, 'get_whiteboard_history'],
'permission_callback' => [$this, 'check_whiteboard_permission']
]
]);
register_rest_route('collab-whiteboard/v1', '/whiteboard/(?P<id>d+)/chat', [
[
'methods' => 'GET',
'callback' => [$this, 'get_chat_messages'],
'permission_callback' => [$this, 'check_whiteboard_permission']
],
[
'methods' => 'POST',
'callback' => [$this, 'post_chat_message'],
'permission_callback' => [$this, 'check_comment_permission']
]
]);
}
public function get_whiteboard($request) {
$whiteboard_id = $request['id'];
global $wpdb;
$table_name = $wpdb->prefix . 'collab_whiteboards';
$whiteboard = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $table_name WHERE id = %d",
$whiteboard_id
));
if (!$whiteboard) {
return new WP_Error('not_found', '白板不存在', ['status' => 404]);
}
return rest_ensure_response([
'id' => $whiteboard->id,
'title' => $whiteboard->title,
'description' => $whiteboard->description,
'content' => json_decode($whiteboard->content, true),
'settings' => json_decode($whiteboard->settings, true),
'created_by' => $whiteboard->created_by,
'created_at' => $whiteboard->created_at,
'updated_at' => $whiteboard->updated_at,
'is_public' => (bool)$whiteboard->is_public
]);
}
public function update_whiteboard($request) {
$whiteboard_id = $request['id'];
$content = $request->get_param('content');
if (empty($content)) {
return new WP_Error('invalid_data', '内容不能为空', ['status' => 400]);
}
global $wpdb;
$table_name = $wpdb->prefix . 'collab_whiteboards';
$result = $wpdb->update(
$table_name,
[
'content' => json_encode($content),
'updated_at' => current_time('mysql')
],
['id' => $whiteboard_id]
);
if ($result === false) {
return new WP_Error('update_failed', '更新失败', ['status' => 500]);
}
return rest_ensure_response([
'success' => true,
'message' => '白板已更新'
]);
}
public function check_whiteboard_permission($request) {
$whiteboard_id = $request['id'];
$user_id = get_current_user_id();
return Whiteboard_Permissions::check_permission(
$whiteboard_id,
$user_id,
Whiteboard_Permissions::PERMISSION_VIEW


