文章目录[隐藏]
一步步实现:为WordPress打造内嵌的在线流程图与UI原型设计工具
引言:为什么WordPress需要内置设计工具?
在当今数字化时代,网站不仅仅是信息展示平台,更是用户体验和交互设计的重要载体。对于WordPress用户而言,虽然市面上有众多第三方设计工具,但频繁切换平台、格式兼容性问题以及额外成本常常成为工作流程中的痛点。想象一下,如果能在WordPress编辑器中直接创建流程图、线框图和UI原型,将极大提升内容创作效率和协作便利性。
本文将通过详细的代码实现步骤,展示如何为WordPress开发一个内嵌的在线设计工具,让用户无需离开WordPress环境就能完成专业的设计工作。我们将从需求分析开始,逐步深入到架构设计、核心功能实现和优化方案,最终打造一个功能完善、性能优异的WordPress设计工具插件。
第一章:项目规划与需求分析
1.1 核心功能需求
在开始编码之前,我们需要明确工具的核心功能:
- 流程图绘制:支持基本形状、连接线、文本标注
- UI原型设计:提供常用UI组件库(按钮、输入框、导航栏等)
- 实时协作:支持多用户同时编辑(可选高级功能)
- 导出功能:支持PNG、SVG、PDF格式导出
- 版本控制:设计稿的版本管理和回溯
- 与WordPress内容集成:可将设计直接插入文章或页面
1.2 技术选型与架构设计
考虑到工具需要在浏览器中运行,我们选择以下技术栈:
- 前端框架:React + TypeScript(提供良好的组件化开发和类型安全)
- 绘图库:Fabric.js 或 Konva.js(处理Canvas绘图操作)
- 后端:WordPress REST API + 自定义端点
- 数据存储:WordPress数据库自定义表 + 文件系统存储
- 实时协作:WebSocket(使用Pusher或Socket.io)
1.3 开发环境搭建
首先,我们需要设置WordPress插件开发环境:
<?php
/**
* Plugin Name: WP Design Studio
* Plugin URI: https://example.com/wp-design-studio
* Description: 内嵌的在线流程图与UI原型设计工具
* Version: 1.0.0
* Author: Your Name
* License: GPL v2 or later
*/
// 防止直接访问
if (!defined('ABSPATH')) {
exit;
}
// 定义插件常量
define('WPDS_VERSION', '1.0.0');
define('WPDS_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('WPDS_PLUGIN_URL', plugin_dir_url(__FILE__));
第二章:数据库设计与后端开发
2.1 创建自定义数据库表
我们需要存储设计项目的数据结构:
// 在插件激活时创建数据库表
register_activation_hook(__FILE__, 'wpds_create_tables');
function wpds_create_tables() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$table_name = $wpdb->prefix . 'wpds_designs';
$sql = "CREATE TABLE IF NOT EXISTS $table_name (
id bigint(20) NOT NULL AUTO_INCREMENT,
user_id bigint(20) NOT NULL,
title varchar(255) NOT NULL,
type varchar(50) NOT NULL DEFAULT 'flowchart',
content longtext NOT NULL,
thumbnail varchar(500) DEFAULT NULL,
settings text DEFAULT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY user_id (user_id),
KEY type (type)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
// 创建版本历史表
$history_table = $wpdb->prefix . 'wpds_design_history';
$sql_history = "CREATE TABLE IF NOT EXISTS $history_table (
id bigint(20) NOT NULL AUTO_INCREMENT,
design_id bigint(20) NOT NULL,
version int(11) NOT NULL,
content longtext NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
user_id bigint(20) NOT NULL,
PRIMARY KEY (id),
KEY design_id (design_id),
KEY version (version)
) $charset_collate;";
dbDelta($sql_history);
}
2.2 实现REST API端点
为前端提供数据交互接口:
// 注册REST API路由
add_action('rest_api_init', 'wpds_register_rest_routes');
function wpds_register_rest_routes() {
// 获取设计列表
register_rest_route('wpds/v1', '/designs', array(
'methods' => 'GET',
'callback' => 'wpds_get_designs',
'permission_callback' => function () {
return current_user_can('edit_posts');
}
));
// 获取单个设计
register_rest_route('wpds/v1', '/designs/(?P<id>d+)', array(
'methods' => 'GET',
'callback' => 'wpds_get_design',
'permission_callback' => function () {
return current_user_can('edit_posts');
}
));
// 创建/更新设计
register_rest_route('wpds/v1', '/designs', array(
'methods' => 'POST',
'callback' => 'wpds_save_design',
'permission_callback' => function () {
return current_user_can('edit_posts');
}
));
// 删除设计
register_rest_route('wpds/v1', '/designs/(?P<id>d+)', array(
'methods' => 'DELETE',
'callback' => 'wpds_delete_design',
'permission_callback' function () {
return current_user_can('delete_posts');
}
));
}
// 获取设计列表的实现
function wpds_get_designs($request) {
global $wpdb;
$table_name = $wpdb->prefix . 'wpds_designs';
$user_id = get_current_user_id();
$page = $request->get_param('page') ? intval($request->get_param('page')) : 1;
$per_page = $request->get_param('per_page') ? intval($request->get_param('per_page')) : 20;
$offset = ($page - 1) * $per_page;
$designs = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM $table_name WHERE user_id = %d ORDER BY updated_at DESC LIMIT %d OFFSET %d",
$user_id, $per_page, $offset
)
);
$total = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM $table_name WHERE user_id = %d",
$user_id
)
);
return new WP_REST_Response(array(
'designs' => $designs,
'pagination' => array(
'total' => $total,
'pages' => ceil($total / $per_page),
'current' => $page,
'per_page' => $per_page
)
), 200);
}
第三章:前端架构与核心组件开发
3.1 设置React开发环境
由于WordPress传统开发方式与现代前端框架存在差异,我们需要特殊配置:
// webpack.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: {
'wpds-editor': './src/editor/index.tsx',
'wpds-block': './src/block/index.tsx',
},
output: {
path: path.resolve(__dirname, 'assets/js'),
filename: '[name].js',
},
externals: {
'react': 'React',
'react-dom': 'ReactDOM',
'wp': 'wp',
},
module: {
rules: [
{
test: /.(ts|tsx)$/,
exclude: /node_modules/,
use: 'ts-loader',
},
{
test: /.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx'],
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
}),
],
};
3.2 核心编辑器组件
实现设计工具的核心画布组件:
// src/editor/components/DesignCanvas.tsx
import React, { useRef, useEffect, useState } from 'react';
import { FabricCanvas } from './FabricCanvas';
import { Toolbar } from './Toolbar';
import { PropertiesPanel } from './PropertiesPanel';
import { ShapesLibrary } from './ShapesLibrary';
import { saveDesign, loadDesign } from '../services/api';
interface DesignCanvasProps {
designId?: number;
onSave?: (design: any) => void;
}
export const DesignCanvas: React.FC<DesignCanvasProps> = ({ designId, onSave }) => {
const canvasRef = useRef<any>(null);
const [selectedTool, setSelectedTool] = useState<string>('select');
const [selectedObject, setSelectedObject] = useState<any>(null);
const [designData, setDesignData] = useState<any>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
// 加载设计数据
useEffect(() => {
if (designId) {
loadDesignData(designId);
}
}, [designId]);
const loadDesignData = async (id: number) => {
setIsLoading(true);
try {
const data = await loadDesign(id);
setDesignData(data);
if (canvasRef.current) {
canvasRef.current.loadFromJSON(data.content);
}
} catch (error) {
console.error('Failed to load design:', error);
} finally {
setIsLoading(false);
}
};
const handleSave = async () => {
if (!canvasRef.current) return;
const content = canvasRef.current.toJSON();
const design = {
title: designData?.title || '未命名设计',
type: 'flowchart',
content: JSON.stringify(content),
settings: JSON.stringify({}),
};
try {
const savedDesign = await saveDesign(design, designId);
setDesignData(savedDesign);
if (onSave) {
onSave(savedDesign);
}
alert('设计已保存!');
} catch (error) {
console.error('Failed to save design:', error);
alert('保存失败,请重试!');
}
};
const handleAddShape = (shapeType: string) => {
if (!canvasRef.current) return;
const canvas = canvasRef.current;
let shape;
switch (shapeType) {
case 'rectangle':
shape = new fabric.Rect({
left: 100,
top: 100,
width: 100,
height: 60,
fill: '#ffffff',
stroke: '#333333',
strokeWidth: 2,
});
break;
case 'circle':
shape = new fabric.Circle({
left: 100,
top: 100,
radius: 50,
fill: '#ffffff',
stroke: '#333333',
strokeWidth: 2,
});
break;
case 'arrow':
// 箭头实现
break;
default:
return;
}
canvas.add(shape);
canvas.setActiveObject(shape);
setSelectedObject(shape);
canvas.renderAll();
};
return (
<div className="wpds-design-canvas">
{isLoading ? (
<div className="wpds-loading">加载中...</div>
) : (
<>
<Toolbar
selectedTool={selectedTool}
onToolSelect={setSelectedTool}
onSave={handleSave}
/>
<div className="wpds-canvas-container">
<ShapesLibrary onShapeAdd={handleAddShape} />
<FabricCanvas
ref={canvasRef}
selectedTool={selectedTool}
onObjectSelect={setSelectedObject}
/>
<PropertiesPanel
selectedObject={selectedObject}
onPropertyChange={(properties) => {
if (selectedObject && canvasRef.current) {
selectedObject.set(properties);
canvasRef.current.renderAll();
}
}}
/>
</div>
</>
)}
</div>
);
};
3.3 Fabric.js画布集成
// src/editor/components/FabricCanvas.tsx
import React, { forwardRef, useImperativeHandle, useEffect } from 'react';
import { fabric } from 'fabric';
interface FabricCanvasProps {
selectedTool: string;
onObjectSelect: (object: any) => void;
}
export const FabricCanvas = forwardRef((props: FabricCanvasProps, ref) => {
const canvasRef = React.useRef<HTMLCanvasElement>(null);
const fabricCanvasRef = React.useRef<fabric.Canvas | null>(null);
// 初始化画布
useEffect(() => {
if (!canvasRef.current) return;
const canvas = new fabric.Canvas(canvasRef.current, {
width: 800,
height: 600,
backgroundColor: '#f5f5f5',
selection: true,
});
fabricCanvasRef.current = canvas;
// 设置事件监听
canvas.on('selection:created', (e) => {
props.onObjectSelect(e.selected[0]);
});
canvas.on('selection:updated', (e) => {
props.onObjectSelect(e.selected[0]);
});
canvas.on('selection:cleared', () => {
props.onObjectSelect(null);
});
// 根据选择的工具设置画布模式
updateCanvasMode(props.selectedTool);
return () => {
canvas.dispose();
};
}, []);
// 更新工具选择
useEffect(() => {
updateCanvasMode(props.selectedTool);
}, [props.selectedTool]);
const updateCanvasMode = (tool: string) => {
if (!fabricCanvasRef.current) return;
const canvas = fabricCanvasRef.current;
switch (tool) {
case 'select':
canvas.isDrawingMode = false;
canvas.selection = true;
canvas.defaultCursor = 'default';
break;
case 'rectangle':
canvas.isDrawingMode = false;
canvas.selection = false;
canvas.defaultCursor = 'crosshair';
setupRectangleDrawing(canvas);
break;
case 'line':
canvas.isDrawingMode = true;
canvas.freeDrawingBrush = new fabric.PencilBrush(canvas);
canvas.freeDrawingBrush.width = 2;
canvas.freeDrawingBrush.color = '#333333';
break;
case 'text':
canvas.isDrawingMode = false;
canvas.selection = false;
canvas.defaultCursor = 'text';
setupTextTool(canvas);
break;
}
};
const setupRectangleDrawing = (canvas: fabric.Canvas) => {
let rect: fabric.Rect | null = null;
let isDrawing = false;
let startX = 0;
let startY = 0;
canvas.on('mouse:down', (o) => {
isDrawing = true;
const pointer = canvas.getPointer(o.e);
startX = pointer.x;
startY = pointer.y;
rect = new fabric.Rect({
left: startX,
top: startY,
width: 0,
height: 0,
fill: 'transparent',
stroke: '#333333',
strokeWidth: 2,
});
canvas.add(rect);
});
canvas.on('mouse:move', (o) => {
if (!isDrawing || !rect) return;
const pointer = canvas.getPointer(o.e);
const width = pointer.x - startX;
const height = pointer.y - startY;
rect.set({
width: Math.abs(width),
height: Math.abs(height),
left: width > 0 ? startX : pointer.x,
top: height > 0 ? startY : pointer.y,
});
canvas.renderAll();
});
canvas.on('mouse:up', () => {
isDrawing = false;
if (rect && (rect.width === 0 || rect.height === 0)) {
canvas.remove(rect);
}
rect = null;
});
};
const setupTextTool = (canvas: fabric.Canvas) => {
canvas.on('mouse:down', (o) => {
const pointer = canvas.getPointer(o.e);
const text = new fabric.IText('双击编辑文本', {
left: pointer.x,
top: pointer.y,
fontSize: 16,
fill: '#333333',
});
canvas.add(text);
canvas.setActiveObject(text);
text.enterEditing();
text.selectAll();
});
};
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
getCanvas: () => fabricCanvasRef.current,
loadFromJSON: (json: any) => {
if (fabricCanvasRef.current) {
fabricCanvasRef.current.loadFromJSON(json, () => {
fabricCanvasRef.current?.renderAll();
});
}
},
toJSON: () => {
return fabricCanvasRef.current?.toJSON();
},
}));
return (
<div className="wpds-fabric-canvas">
<canvas ref={canvasRef} />
</div>
);
});
第四章:WordPress集成与Gutenberg块开发
4.1 创建Gutenberg块
为了让用户能在文章/页面中插入设计,我们需要创建Gutenberg块:
// src/block/index.js
import { registerBlockType } from '@wordpress/blocks';
import { Button, Modal } from '@wordpress/components';
import { useState } from '@wordpress/element';
import { DesignCanvas } from '../editor/components/DesignCanvas';
/design-block', {
title: '设计图',
icon: 'layout',
category: 'embed',
attributes: {
designId: {
type: 'number',
default: 0
},
designTitle: {
type: 'string',
default: ''
},
thumbnail: {
type: 'string',
default: ''
},
width: {
type: 'string',
default: '100%'
},
align: {
type: 'string',
default: 'center'
}
},
edit: function({ attributes, setAttributes }) {
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedDesign, setSelectedDesign] = useState(null);
// 打开设计库
const openDesignLibrary = async () => {
try {
const response = await fetch('/wp-json/wpds/v1/designs');
const designs = await response.json();
// 这里应该显示设计库模态框
setIsModalOpen(true);
} catch (error) {
console.error('Failed to load designs:', error);
}
};
// 创建新设计
const createNewDesign = () => {
setIsModalOpen(true);
setSelectedDesign(null);
};
// 选择设计
const handleSelectDesign = (design) => {
setAttributes({
designId: design.id,
designTitle: design.title,
thumbnail: design.thumbnail
});
setIsModalOpen(false);
};
// 保存设计
const handleSaveDesign = (design) => {
setAttributes({
designId: design.id,
designTitle: design.title,
thumbnail: design.thumbnail
});
};
return (
<div className={`wpds-design-block align${attributes.align}`}>
{attributes.designId ? (
<div className="wpds-design-preview">
<img
src={attributes.thumbnail || `${wpds_plugin_url}/assets/images/default-thumbnail.png`}
alt={attributes.designTitle}
style={{ width: attributes.width }}
/>
<div className="wpds-design-actions">
<Button isSecondary onClick={() => setIsModalOpen(true)}>
编辑设计
</Button>
<Button isDestructive onClick={() => {
setAttributes({
designId: 0,
designTitle: '',
thumbnail: ''
});
}}>
移除
</Button>
</div>
</div>
) : (
<div className="wpds-design-placeholder">
<p>插入设计图</p>
<div className="wpds-design-buttons">
<Button isPrimary onClick={createNewDesign}>
创建新设计
</Button>
<Button isSecondary onClick={openDesignLibrary}>
从库中选择
</Button>
</div>
</div>
)}
{isModalOpen && (
<Modal
title={selectedDesign ? "编辑设计" : "创建新设计"}
onRequestClose={() => setIsModalOpen(false)}
className="wpds-design-modal"
>
<DesignCanvas
designId={selectedDesign?.id}
onSave={handleSaveDesign}
/>
</Modal>
)}
</div>
);
},
save: function({ attributes }) {
if (!attributes.designId) {
return null;
}
return (
<div className={`wpds-design-embed align${attributes.align}`}>
<div
className="wpds-design-container"
data-design-id={attributes.designId}
style={{ maxWidth: attributes.width }}
>
{/* 这里将渲染实际的设计图 */}
<div className="wpds-design-loading">
加载设计中...
</div>
</div>
</div>
);
}
});
### 4.2 前端渲染设计图
当文章显示时,我们需要在前端渲染设计图:
// src/frontend/render.js
document.addEventListener('DOMContentLoaded', function() {
// 查找所有设计图容器
const designContainers = document.querySelectorAll('.wpds-design-container[data-design-id]');
designContainers.forEach(container => {
const designId = container.getAttribute('data-design-id');
loadAndRenderDesign(designId, container);
});
});
async function loadAndRenderDesign(designId, container) {
try {
// 获取设计数据
const response = await fetch(`/wp-json/wpds/v1/designs/${designId}`);
const design = await response.json();
// 创建Canvas元素
const canvas = document.createElement('canvas');
canvas.width = 800;
canvas.height = 600;
canvas.className = 'wpds-rendered-canvas';
container.innerHTML = '';
container.appendChild(canvas);
// 使用Fabric.js渲染设计
const fabricCanvas = new fabric.Canvas(canvas);
fabricCanvas.loadFromJSON(JSON.parse(design.content), () => {
fabricCanvas.renderAll();
// 添加交互功能
if (design.type === 'prototype') {
addPrototypeInteractions(fabricCanvas, design);
}
});
} catch (error) {
console.error('Failed to render design:', error);
container.innerHTML = '<div class="wpds-design-error">无法加载设计图</div>';
}
}
function addPrototypeInteractions(canvas, design) {
// 为UI原型添加点击交互
canvas.on('mouse:down', function(e) {
if (e.target && e.target.linkTo) {
// 处理原型链接
if (e.target.linkTo.startsWith('#')) {
// 内部页面跳转
const targetScreen = design.screens.find(s => s.id === e.target.linkTo.substring(1));
if (targetScreen) {
// 切换到目标屏幕
canvas.loadFromJSON(targetScreen.content, () => {
canvas.renderAll();
});
}
} else {
// 外部链接
window.open(e.target.linkTo, '_blank');
}
}
});
// 添加悬停效果
canvas.on('mouse:over', function(e) {
if (e.target && e.target.linkTo) {
canvas.defaultCursor = 'pointer';
e.target.set('stroke', '#007cba');
canvas.renderAll();
}
});
canvas.on('mouse:out', function(e) {
if (e.target && e.target.linkTo) {
canvas.defaultCursor = 'default';
e.target.set('stroke', e.target.originalStroke || '#333333');
canvas.renderAll();
}
});
}
## 第五章:UI组件库与模板系统
### 5.1 构建UI组件库
// src/editor/components/UIComponentsLibrary.tsx
import React from 'react';
const UI_COMPONENTS = {
basic: [
{ id: 'button', name: '按钮', icon: 'button', component: ButtonComponent },
{ id: 'input', name: '输入框', icon: 'input', component: InputComponent },
{ id: 'textarea', name: '文本域', icon: 'textarea', component: TextareaComponent },
{ id: 'dropdown', name: '下拉菜单', icon: 'dropdown', component: DropdownComponent },
],
layout: [
{ id: 'header', name: '页眉', icon: 'header', component: HeaderComponent },
{ id: 'footer', name: '页脚', icon: 'footer', component: FooterComponent },
{ id: 'sidebar', name: '侧边栏', icon: 'sidebar', component: SidebarComponent },
{ id: 'navbar', name: '导航栏', icon: 'navbar', component: NavbarComponent },
],
mobile: [
{ id: 'mobile-header', name: '移动端页眉', icon: 'smartphone', component: MobileHeaderComponent },
{ id: 'tab-bar', name: '标签栏', icon: 'tab-bar', component: TabBarComponent },
{ id: 'list-item', name: '列表项', icon: 'list', component: ListItemComponent },
]
};
export const UIComponentsLibrary: React.FC<{ onComponentAdd: (component: any) => void }> = ({ onComponentAdd }) => {
const [activeCategory, setActiveCategory] = React.useState('basic');
const handleDragStart = (e: React.DragEvent, component: any) => {
e.dataTransfer.setData('application/wpds-component', JSON.stringify(component));
};
return (
<div className="wpds-ui-library">
<div className="wpds-ui-categories">
{Object.keys(UI_COMPONENTS).map(category => (
<button
key={category}
className={`wpds-category-btn ${activeCategory === category ? 'active' : ''}`}
onClick={() => setActiveCategory(category)}
>
{category === 'basic' ? '基础' :
category === 'layout' ? '布局' : '移动端'}
</button>
))}
</div>
<div className="wpds-components-grid">
{UI_COMPONENTS[activeCategory].map(component => (
<div
key={component.id}
className="wpds-component-item"
draggable
onDragStart={(e) => handleDragStart(e, component)}
onClick={() => onComponentAdd(component)}
>
<div className="wpds-component-icon">
{/* 这里放置图标 */}
</div>
<span className="wpds-component-name">{component.name}</span>
</div>
))}
</div>
</div>
);
};
// 按钮组件定义
const ButtonComponent = {
type: 'button',
config: {
text: '按钮',
width: 100,
height: 40,
backgroundColor: '#007cba',
color: '#ffffff',
borderRadius: 4,
fontSize: 14,
},
create: function(config = {}) {
return new fabric.Rect({
width: config.width || 100,
height: config.height || 40,
fill: config.backgroundColor || '#007cba',
rx: config.borderRadius || 4,
ry: config.borderRadius || 4,
strokeWidth: 0,
data: {
type: 'button',
config: config
}
});
}
};
### 5.2 模板系统实现
// includes/class-templates.php
class WPDS_Templates {
private static $instance = null;
private $templates = [];
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
$this->load_default_templates();
add_action('init', [$this, 'register_template_post_type']);
}
public function register_template_post_type() {
register_post_type('wpds_template', [
'labels' => [
'name' => __('设计模板', 'wpds'),
'singular_name' => __('模板', 'wpds'),
],
'public' => false,
'show_ui' => true,
'show_in_menu' => 'wpds-design-studio',
'supports' => ['title', 'thumbnail'],
'capability_type' => 'post',
]);
}
private function load_default_templates() {
$this->templates = [
'flowchart-basic' => [
'name' => '基础流程图',
'type' => 'flowchart',
'thumbnail' => WPDS_PLUGIN_URL . 'assets/templates/flowchart-basic.png',
'content' => json_encode([
'objects' => [
// 默认的流程图元素
],
'background' => '#ffffff'
])
],
'website-wireframe' => [
'name' => '网站线框图',
'type' => 'wireframe',
'thumbnail' => WPDS_PLUGIN_URL . 'assets/templates/website-wireframe.png',
'content' => json_encode([
'objects' => [
// 网站线框图元素
],
'background' => '#f5f5f5'
])
],
'mobile-app' => [
'name' => '移动应用原型',
'type' => 'prototype',
'thumbnail' => WPDS_PLUGIN_URL . 'assets/templates/mobile-app.png',
'content' => json_encode([
'screens' => [
// 多个屏幕定义
]
])
]
];
}
public function get_templates($type = '') {
if ($type) {
return array_filter($this->templates, function($template) use ($type) {
return $template['type'] === $type;
});
}
return $this->templates;
}
public function apply_template($template_id, $canvas) {
if (!isset($this->templates[$template_id])) {
return false;
}
$template = $this->templates[$template_id];
$canvas.loadFromJSON(json_decode($template['content'], true));
return true;
}
}
## 第六章:高级功能实现
### 6.1 实时协作功能
// src/collaboration/CollaborationManager.js
import Pusher from 'pusher-js';
export class CollaborationManager {
constructor(designId, userId) {
this.designId = designId;
this.userId = userId;
this.canvas = null;
this.pusher = null;
this.channel = null;
this.cursors = new Map();
this.initializePusher();
}
initializePusher() {
// 初始化Pusher连接
this.pusher = new Pusher(wpds_pusher_key, {
cluster: wpds_pusher_cluster,
authEndpoint: '/wp-json/wpds/v1/pusher/auth',
auth: {
params: {
user_id: this.userId,
design_id: this.designId
}
}
});
this.channel = this.pusher.subscribe(`private-design-${this.designId}`);
// 监听其他用户的操作
this.channel.bind('client-object-added', this.handleObjectAdded.bind(this));
this.channel.bind('client-object-modified', this.handleObjectModified.bind(this));
this.channel.bind('client-object-removed', this.handleObjectRemoved.bind(this));
this.channel.bind('client-cursor-moved', this.handleCursorMoved.bind(this));
}
setCanvas(canvas) {
this.canvas = canvas;
this.setupCanvasEvents();
}
setupCanvasEvents() {
if (!this.canvas) return;
// 监听画布变化并广播
this.canvas.on('object:added', (e) => {
if (e.target.__local) return; // 避免循环
this.broadcast('object-added', e.target.toJSON());
});
this.canvas.on('object:modified', (e) => {
if (e.target.__local) return;
this.broadcast('object-modified', {
id: e.target.id,
properties: e.target.toJSON()
});
});
this.canvas.on('object:removed', (e) => {
if (e.target.__local) return;
this.broadcast('object-removed', e.target.id);
});
// 监听鼠标移动
this.canvas.on('mouse:move', (e) => {
const pointer = this.canvas.getPointer(e.e);
this.broadcast('cursor-moved', {
x: pointer.x,
y: pointer.y,
userId: this.userId
});
});
}
broadcast(event, data) {
if (!this.channel) return;
data.timestamp = Date.now();
data.userId = this.userId;
this.channel.trigger(`client-${event}`, data);
}
handleObjectAdded(data) {
if (data.userId === this.userId) return;
fabric.util.enlivenObjects([data], (objects) => {
objects.forEach(obj => {
obj.__remote = true;
this.canvas.add(obj);
});
this.canvas.renderAll();
});
}
handleObjectModified(data) {
if (data.userId === this.userId) return;
const obj = this.canvas.getObjects().find(o => o.id === data.id);
if (obj) {
obj.__remote = true;
obj.set(data.properties);
this.canvas.renderAll();
}
}
handleCursorMoved(data) {
if (data.userId === this.userId) return;
this.updateUserCursor(data.userId, data.x, data.y);
}
updateUserCursor(userId, x, y) {
let cursor = this.cursors.get(userId);
if (!cursor) {
// 创建新的光标
cursor = new fabric.Circle({
radius: 5,
fill: this.getUserColor(userId),
left: x,
top: y,
selectable: false,
hasControls: false,
hasBorders: false
});
// 添加用户标签
const text = new fabric.Text(`用户${userId}`, {
left: x + 10,
top: y - 10,
fontSize: 12,
fill: this.getUserColor(userId)
});
cursor.label = text;
this.canvas.add(cursor);
this.canvas.add(text);
this.cursors.set(userId, { cursor, label: text });
} else {
// 更新现有光标位置
cursor.cursor.set({ left: x, top: y });
cursor.label.set({ left: x + 10, top: y - 10 });
}
this.canvas.renderAll();
}
getUserColor(userId) {
const colors = ['#FF6B6B', '#4ECDC4', '#FFD166', '#06D6A0', '#118AB2'];
return colors[userId % colors.length];
}
destroy() {
if (this.pusher) {
this.pusher.disconnect();
}
}
}
