文章目录
-
- 本文提供了一份详细的技术指南,介绍如何在WordPress平台中通过代码二次开发,集成一个在线简易PSD文件查看与标注工具。我们将从需求分析开始,逐步讲解技术选型、开发流程、核心功能实现以及优化建议,帮助开发者掌握在WordPress中扩展专业功能的方法。
- 引言:为什么在WordPress中集成PSD查看与标注工具 技术选型与准备工作 WordPress插件架构设计 前端PSD查看器实现 标注功能开发 用户权限与文件管理 性能优化与安全考虑 测试与部署 扩展功能建议 结论
-
- WordPress作为全球最流行的内容管理系统,不仅用于博客和网站建设,其强大的插件机制和可扩展性使其成为各种专业应用的理想平台。通过二次开发,我们可以将专业工具集成到WordPress中,为用户提供一体化的解决方案。
- 对于设计团队、客户协作和在线教育等场景,能够直接在网页中查看PSD文件并进行标注可以极大提高工作效率: 设计师与客户之间的设计评审 团队内部的设计协作 在线设计课程的素材展示 设计稿版本对比与反馈收集
- 虽然市场上有一些在线设计工具,但它们往往需要付费、功能过于复杂或无法与WordPress无缝集成。通过自主开发,我们可以创建轻量级、定制化的解决方案,完美融入现有WordPress环境。
-
- 在开始开发前,需要准备以下环境: # 本地开发环境 - WordPress 5.8+ 安装 - PHP 7.4+ 环境 - MySQL 5.6+ 或 MariaDB 10.1+ - 代码编辑器(VS Code、PHPStorm等) - 浏览器开发者工具
-
- 考虑到PSD文件的复杂性,我们需要选择合适的解析库: PSD.js - 基于JavaScript的PSD解析器,适合前端处理 ImageMagick/GraphicsMagick - 服务器端处理方案 Photoshop API - Adobe官方API(功能强大但成本较高) 对于简易查看器,我们推荐使用PSD.js,因为它: 纯前端实现,减轻服务器负担 开源免费,社区活跃 支持图层提取和基本信息读取
- Fabric.js - 强大的Canvas操作库 Konva.js - 另一个优秀的Canvas库 自定义Canvas实现 - 更轻量但开发成本高 我们选择Fabric.js,因为它提供了丰富的图形对象和交互功能。
- 我们将采用标准的WordPress插件开发模式: 遵循WordPress编码标准 使用WordPress REST API进行前后端通信 利用WordPress的媒体库进行文件管理
- 创建插件基础目录结构: wp-psd-viewer-annotator/ ├── wp-psd-viewer-annotator.php # 主插件文件 ├── includes/ # 核心功能文件 │ ├── class-psd-handler.php # PSD处理类 │ ├── class-annotation-manager.php # 标注管理类 │ └── class-file-manager.php # 文件管理类 ├── admin/ # 后台管理文件 │ ├── css/ │ ├── js/ │ └── views/ ├── public/ # 前端文件 │ ├── css/ │ ├── js/ │ └── views/ ├── assets/ # 静态资源 │ ├── psd.js # PSD解析库 │ └── fabric.js # 标注库 └── languages/ # 国际化文件
-
- <?php /** * Plugin Name: PSD Viewer & Annotator for WordPress * Plugin URI: https://yourwebsite.com/ * Description: 在WordPress中查看和标注PSD文件的工具 * Version: 1.0.0 * Author: Your Name * License: GPL v2 or later */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('PSD_VA_VERSION', '1.0.0'); define('PSD_VA_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('PSD_VA_PLUGIN_URL', plugin_dir_url(__FILE__)); define('PSD_VA_MAX_FILE_SIZE', 104857600); // 100MB // 自动加载类文件 spl_autoload_register(function ($class_name) { $prefix = 'PSD_VA_'; $base_dir = PSD_VA_PLUGIN_DIR . 'includes/'; if (strpos($class_name, $prefix) !== 0) { return; } $relative_class = substr($class_name, strlen($prefix)); $file = $base_dir . 'class-' . strtolower(str_replace('_', '-', $relative_class)) . '.php'; if (file_exists($file)) { require_once $file; } }); // 初始化插件 function psd_va_init() { // 检查依赖 if (!function_exists('gd_info')) { add_action('admin_notices', function() { echo '<div class="notice notice-error"><p>PSD查看器需要GD库支持,请启用PHP的GD扩展。</p></div>'; }); return; } // 初始化核心类 $psd_handler = new PSD_VA_PSD_Handler(); $annotation_manager = new PSD_VA_Annotation_Manager(); $file_manager = new PSD_VA_File_Manager(); // 注册短代码 add_shortcode('psd_viewer', array($psd_handler, 'shortcode_handler')); // 注册REST API端点 add_action('rest_api_init', array($annotation_manager, 'register_rest_routes')); // 注册管理菜单 add_action('admin_menu', 'psd_va_admin_menu'); } add_action('plugins_loaded', 'psd_va_init'); // 管理菜单 function psd_va_admin_menu() { add_menu_page( 'PSD查看器', 'PSD查看器', 'manage_options', 'psd-viewer', 'psd_va_admin_page', 'dashicons-format-image', 30 ); } function psd_va_admin_page() { include PSD_VA_PLUGIN_DIR . 'admin/views/admin-page.php'; } // 激活/停用钩子 register_activation_hook(__FILE__, 'psd_va_activate'); register_deactivation_hook(__FILE__, 'psd_va_deactivate'); function psd_va_activate() { // 创建必要的数据库表 global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $annotations_table = $wpdb->prefix . 'psd_va_annotations'; $sql = "CREATE TABLE IF NOT EXISTS $annotations_table ( id bigint(20) NOT NULL AUTO_INCREMENT, psd_id bigint(20) NOT NULL, user_id bigint(20) NOT NULL, annotation_data longtext NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY psd_id (psd_id), KEY user_id (user_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 设置默认选项 add_option('psd_va_max_file_size', PSD_VA_MAX_FILE_SIZE); add_option('psd_va_allowed_roles', array('administrator', 'editor', 'author')); } function psd_va_deactivate() { // 清理临时文件 $upload_dir = wp_upload_dir(); $temp_dir = $upload_dir['basedir'] . '/psd-va-temp/'; if (is_dir($temp_dir)) { array_map('unlink', glob($temp_dir . '*')); rmdir($temp_dir); } }
- 我们需要创建以下数据库表来存储标注信息: -- 标注数据表 CREATE TABLE wp_psd_va_annotations ( id BIGINT(20) NOT NULL AUTO_INCREMENT, psd_id BIGINT(20) NOT NULL, -- 关联的PSD文件ID user_id BIGINT(20) NOT NULL, -- 创建标注的用户ID annotation_data LONGTEXT NOT NULL, -- 标注数据(JSON格式) created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), INDEX psd_id_idx (psd_id), INDEX user_id_idx (user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
- // 在插件中注册脚本 function psd_va_enqueue_scripts() { // 前端样式 wp_enqueue_style( 'psd-va-frontend', PSD_VA_PLUGIN_URL . 'public/css/frontend.css', array(), PSD_VA_VERSION ); // 核心库 wp_enqueue_script( 'psd-js', PSD_VA_PLUGIN_URL . 'assets/js/psd.min.js', array(), '0.8.0', true ); wp_enqueue_script( 'fabric-js', PSD_VA_PLUGIN_URL . 'assets/js/fabric.min.js', array(), '4.5.0', true ); // 主脚本 wp_enqueue_script( 'psd-va-main', PSD_VA_PLUGIN_URL . 'public/js/main.js', array('jquery', 'psd-js', 'fabric-js'), PSD_VA_VERSION, true ); // 本地化脚本 wp_localize_script('psd-va-main', 'psd_va_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('psd_va_nonce'), 'rest_url' => rest_url('psd-va/v1/'), 'max_file_size' => get_option('psd_va_max_file_size', PSD_VA_MAX_FILE_SIZE) )); } add_action('wp_enqueue_scripts', 'psd_va_enqueue_scripts');
- // public/js/main.js - PSD查看器核心功能 class PSDViewer { constructor(containerId, options = {}) { this.container = document.getElementById(containerId); this.options = Object.assign({ psdUrl: '', width: 800, height: 600, showLayers: true, allowDownload: true }, options); this.canvas = null; this.psd = null; this.layers = []; this.currentScale = 1; this.init(); } async init() { // 创建UI结构 this.createUI(); // 加载PSD文件 if (this.options.psdUrl) { await this.loadPSD(this.options.psdUrl); } } createUI() { // 创建主容器 this.container.innerHTML = ` <div class="psd-viewer-container"> <div class="psd-toolbar"> <button class="tool-btn zoom-in" title="放大">+</button> <button class="tool-btn zoom-out" title="缩小">-</button> <button class="tool-btn reset-zoom" title="重置缩放">1:1</button> <span class="zoom-level">100%</span> <div class="tool-separator"></div> <button class="tool-btn toggle-layers" title="显示/隐藏图层">图层</button> <button class="tool-btn download-image" title="下载为PNG">下载</button> </div> <div class="psd-main-area"> <div class="psd-canvas-container"> <canvas id="psd-canvas-${this.container.id}"></canvas> </div> <div class="psd-layers-panel"> <h3>图层</h3> <div class="layers-list"></div> </div> </div> <div class="psd-status-bar"> <span class="file-info"></span> <span class="canvas-size"></span> </div> </div> `; // 获取Canvas元素 this.canvas = document.getElementById(`psd-canvas-${this.container.id}`); this.ctx = this.canvas.getContext('2d'); // 绑定事件 this.bindEvents(); } async loadPSD(url) { try { // 显示加载状态 this.showLoading(); // 获取PSD文件 const response = await fetch(url); const arrayBuffer = await response.arrayBuffer(); // 解析PSD this.psd = PSD.fromArrayBuffer(arrayBuffer); this.psd.parse(); // 渲染PSD this.renderPSD(); // 提取图层信息 this.extractLayers(); // 更新UI this.updateFileInfo(); } catch (error) { console.error('加载PSD失败:', error); this.showError('无法加载PSD文件: ' + error.message); } } renderPSD() { if (!this.psd) return; // 获取PSD尺寸 const width = this.psd.header.width; const height = this.psd.header.height; // 设置Canvas尺寸 this.canvas.width = width; this.canvas.height = height; // 渲染到Canvas const imageData = this.psd.image.toCanvas(); this.ctx.drawImage(imageData, 0, 0); // 更新Canvas显示尺寸 this.fitToContainer(); } extractLayers() { if (!this.psd || !this.options.showLayers) return; this.layers = []; const extractLayerInfo = (layer, depth = 0) => { if (layer.visible === false) return; const layerInfo = { id: layer.id || Math.random().toString(36).substr(2, 9), name: layer.name || '未命名图层', visible: layer.visible, opacity: layer.opacity, depth: depth, children: [] }; if (layer.children && layer.children.length > 0) { layer.children.forEach(child => { extractLayerInfo(child, depth + 1); }); } this.layers.push(layerInfo); }; extractLayerInfo(this.psd.tree()); this.renderLayersList(); } renderLayersList() { const layersList = this.container.querySelector('.layers-list'); layersList.innerHTML = ''; this.layers.forEach(layer => { const layerItem = document.createElement('div'); layerItem.className = 'layer-item'; layerItem.style.paddingLeft = (layer.depth * 20) + 'px'; layerItem.innerHTML = ` <label> <input type="checkbox" ${layer.visible ? 'checked' : ''} data-layer-id="${layer.id}"> ${layer.name} </label> `; layersList.appendChild(layerItem); }); } fitToContainer() { const container = this.canvas.parentElement; const containerWidth = container.clientWidth; const containerHeight = container.clientHeight; const psdWidth = this.canvas.width; const psdHeight = this.canvas.height; // 计算适合容器的缩放比例 const scaleX = containerWidth / psdWidth; const scaleY = containerHeight / psdHeight; this.currentScale = Math.min(scaleX, scaleY, 1); // 应用缩放 this.canvas.style.width = (psdWidth * this.currentScale) + 'px'; this.canvas.style.height = (psdHeight * this.currentScale) + 'px'; // 更新缩放显示 this.updateZoomDisplay(); } updateZoomDisplay() { const zoomElement = this.container.querySelector('.zoom-level'); if (zoomElement) { zoomElement.textContent = Math.round(this.currentScale * 100) + '%'; } } bindEvents() { // 缩放按钮 this.container.querySelector('.zoom-in').addEventListener('click', () => { this.zoom(0.1); }); this.container.querySelector('.zoom-out').addEventListener('click', () => { this.zoom(-0.1); }); this.container.querySelector('.reset-zoom').addEventListener('click', () => { this.currentScale = 1; this.canvas.style.width = this.canvas.width + 'px'; .style.height = this.canvas.height + 'px'; this.updateZoomDisplay(); }); // 图层显示/隐藏 this.container.querySelector('.toggle-layers').addEventListener('click', () => { const panel = this.container.querySelector('.psd-layers-panel'); panel.classList.toggle('collapsed'); }); // 下载功能 this.container.querySelector('.download-image').addEventListener('click', () => { this.downloadAsPNG(); }); // 图层复选框事件委托 this.container.querySelector('.layers-list').addEventListener('change', (e) => { if (e.target.type === 'checkbox') { const layerId = e.target.dataset.layerId; this.toggleLayerVisibility(layerId, e.target.checked); } }); // Canvas拖拽和缩放 this.setupCanvasInteractions(); } zoom(delta) { this.currentScale = Math.max(0.1, Math.min(5, this.currentScale + delta)); this.canvas.style.width = (this.canvas.width * this.currentScale) + 'px'; this.canvas.style.height = (this.canvas.height * this.currentScale) + 'px'; this.updateZoomDisplay(); } downloadAsPNG() { const link = document.createElement('a'); link.download = 'psd-export.png'; link.href = this.canvas.toDataURL('image/png'); link.click(); } toggleLayerVisibility(layerId, visible) { // 这里可以实现图层显示/隐藏逻辑 console.log(`图层 ${layerId} 可见性: ${visible}`); // 实际实现需要重新渲染PSD并隐藏/显示特定图层 } setupCanvasInteractions() { let isDragging = false; let lastX = 0; let lastY = 0; this.canvas.addEventListener('mousedown', (e) => { isDragging = true; lastX = e.clientX; lastY = e.clientY; this.canvas.style.cursor = 'grabbing'; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; const deltaX = e.clientX - lastX; const deltaY = e.clientY - lastY; // 更新Canvas位置 const currentLeft = parseInt(this.canvas.style.left || 0); const currentTop = parseInt(this.canvas.style.top || 0); this.canvas.style.left = (currentLeft + deltaX) + 'px'; this.canvas.style.top = (currentTop + deltaY) + 'px'; lastX = e.clientX; lastY = e.clientY; }); document.addEventListener('mouseup', () => { isDragging = false; this.canvas.style.cursor = 'grab'; }); // 鼠标滚轮缩放 this.canvas.addEventListener('wheel', (e) => { e.preventDefault(); const delta = e.deltaY > 0 ? -0.1 : 0.1; this.zoom(delta); }); } showLoading() { this.container.querySelector('.psd-canvas-container').innerHTML = ` <div class="loading-spinner"> <div class="spinner"></div> <p>加载PSD文件中...</p> </div> `; } showError(message) { this.container.querySelector('.psd-canvas-container').innerHTML = ` <div class="error-message"> <p>${message}</p> <button class="retry-btn">重试</button> </div> `; // 重试按钮事件 this.container.querySelector('.retry-btn').addEventListener('click', () => { this.loadPSD(this.options.psdUrl); }); } updateFileInfo() { if (!this.psd) return; const fileInfo = this.container.querySelector('.file-info'); const canvasSize = this.container.querySelector('.canvas-size'); if (fileInfo) { fileInfo.textContent = `尺寸: ${this.psd.header.width} × ${this.psd.header.height} 像素 | 颜色模式: ${this.psd.header.mode}`; } if (canvasSize) { canvasSize.textContent = `缩放: ${Math.round(this.currentScale * 100)}%`; } } } // 初始化查看器document.addEventListener('DOMContentLoaded', function() { const psdContainers = document.querySelectorAll('.psd-viewer'); psdContainers.forEach(container => { const psdUrl = container.dataset.psdUrl; const options = { psdUrl: psdUrl, showLayers: container.dataset.showLayers !== 'false', allowDownload: container.dataset.allowDownload !== 'false' }; new PSDViewer(container.id, options); }); }); ### 4.3 前端样式设计 / public/css/frontend.css /.psd-viewer-container { width: 100%; height: 600px; border: 1px solid #ddd; border-radius: 4px; overflow: hidden; display: flex; flex-direction: column; background: #f5f5f5; } .psd-toolbar { background: #fff; border-bottom: 1px solid #ddd; padding: 10px; display: flex; align-items: center; gap: 10px; flex-shrink: 0; } .tool-btn { padding: 6px 12px; background: #f0f0f0; border: 1px solid #ccc; border-radius: 3px; cursor: pointer; font-size: 14px; transition: all 0.2s; } .tool-btn:hover { background: #e0e0e0; border-color: #999; } .tool-separator { width: 1px; height: 20px; background: #ddd; margin: 0 10px; } .zoom-level { font-size: 14px; color: #666; min-width: 50px; } .psd-main-area { flex: 1; display: flex; overflow: hidden; } .psd-canvas-container { flex: 1; position: relative; overflow: auto; background: linear-gradient(45deg, #eee 25%, transparent 25%), linear-gradient(-45deg, #eee 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #eee 75%), linear-gradient(-45deg, transparent 75%, #eee 75%); background-size: 20px 20px; background-position: 0 0, 0 10px, 10px -10px, -10px 0px; }
- display: block; position: absolute; top: 0; left: 0; cursor: grab; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } .psd-layers-panel { width: 250px; background: #fff; border-left: 1px solid #ddd; padding: 15px; overflow-y: auto; transition: width 0.3s; } .psd-layers-panel.collapsed { width: 0; padding: 0; border: none; overflow: hidden; } .psd-layers-panel h3 { margin-top: 0; margin-bottom: 15px; font-size: 16px; color: #333; } .layers-list { max-height: 400px; overflow-y: auto; } .layer-item { padding: 8px 0; border-bottom: 1px solid #f0f0f0; } .layer-item label { display: flex; align-items: center; cursor: pointer; font-size: 14px; } .layer-item input[type="checkbox"] { margin-right: 8px; } .psd-status-bar { background: #fff; border-top: 1px solid #ddd; padding: 8px 15px; display: flex; justify-content: space-between; font-size: 12px; color: #666; flex-shrink: 0; } .loading-spinner { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; } .spinner { width: 40px; height: 40px; border: 3px solid #f3f3f3; border-top: 3px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 15px; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .error-message { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; color: #e74c3c; } .retry-btn { margin-top: 10px; padding: 8px 16px; background: #3498db; color: white; border: none; border-radius: 3px; cursor: pointer; } / 响应式设计 /@media (max-width: 768px) { .psd-viewer-container { height: 400px; } .psd-layers-panel { position: absolute; right: 0; top: 0; bottom: 0; background: rgba(255, 255, 255, 0.95); z-index: 100; } .psd-toolbar { flex-wrap: wrap; gap: 5px; } } ## 5. 标注功能开发 ### 5.1 标注工具类实现 // public/js/annotation.jsclass PSDAnnotator { constructor(canvasElement, options = {}) { this.canvas = canvasElement; this.fabricCanvas = null; this.annotations = []; this.currentTool = 'select'; this.currentColor = '#ff0000'; this.currentStrokeWidth = 2; this.options = Object.assign({ enableText: true, enableArrow: true, enableRectangle: true, enableCircle: true, enableFreeDraw: true }, options); this.initFabricCanvas(); this.setupAnnotationTools(); } initFabricCanvas() { // 创建Fabric.js Canvas this.fabricCanvas = new fabric.Canvas(this.canvas, { selection: true, preserveObjectStacking: true, backgroundColor: 'transparent' }); // 设置Canvas尺寸与底层PSD Canvas一致 const psdCanvas = document.getElementById(this.canvas.id.replace('annotation-', '')); if (psdCanvas) { this.fabricCanvas.setWidth(psdCanvas.width); this.fabricCanvas.setHeight(psdCanvas.height); this.fabricCanvas.setDimensions({ width: psdCanvas.style.width, height: psdCanvas.style.height }); } // 绑定事件 this.bindCanvasEvents(); } setupAnnotationTools() { this.tools = { select: () => { this.fabricCanvas.isDrawingMode = false; this.fabricCanvas.selection = true; this.fabricCanvas.defaultCursor = 'default'; }, text: () => { this.fabricCanvas.isDrawingMode = false; this.fabricCanvas.selection = false; this.fabricCanvas.defaultCursor = 'text'; this.fabricCanvas.on('mouse:down', (options) => { if (options.target) return; const point = this.fabricCanvas.getPointer(options.e); const text = new fabric.IText('输入文字', { left: point.x, top: point.y, fontSize: 16, fill: this.currentColor, fontFamily: 'Arial' }); this.fabricCanvas.add(text); this.fabricCanvas.setActiveObject(text); text.enterEditing(); text.selectAll(); }); }, rectangle: () => { this.fabricCanvas.isDrawingMode = false; this.fabricCanvas.selection = false; this.fabricCanvas.defaultCursor = 'crosshair'; let rect, isDown, origX, origY; this.fabricCanvas.on('mouse:down', (options) => { isDown = true; const pointer = this.fabricCanvas.getPointer(options.e); origX = pointer.x; origY = pointer.y; rect = new fabric.Rect({ left: origX, top: origY, width: 0, height: 0, fill: 'transparent', stroke: this.currentColor, strokeWidth: this.currentStrokeWidth }); this.fabricCanvas.add(rect); }); this.fabricCanvas.on('mouse:move', (options) => { if (!isDown) return; const pointer = this.fabricCanvas.getPointer(options.e); if (origX > pointer.x) { rect.set({ left: pointer.x }); } if (origY > pointer.y) { rect.set({ top: pointer.y }); } rect.set({ width: Math.abs(origX - pointer.x), height: Math.abs(origY - pointer.y) }); this.fabricCanvas.renderAll(); }); this.fabricCanvas.on('mouse:up', () => { isDown = false; this.saveAnnotation(); }); }, circle: () => { this.fabricCanvas.isDrawingMode = false; this.fabricCanvas.selection = false; this.fabricCanvas.defaultCursor = 'crosshair'; let circle, isDown, origX, origY; this.fabricCanvas.on('mouse:down', (options) => { isDown = true; const pointer = this.fabricCanvas.getPointer(options.e); origX = pointer.x; origY = pointer.y; circle = new fabric.Circle({ left: origX, top: origY, radius: 0, fill: 'transparent', stroke: this.currentColor, strokeWidth: this.currentStrokeWidth }); this.fabricCanvas.add(circle); }); this.fabricCanvas.on('mouse:move', (options) => { if (!isDown) return; const pointer = this.fabricCanvas.getPointer(options.e); const radius = Math.sqrt( Math.pow(origX - pointer.x, 2) + Math.pow(origY - pointer.y, 2) ) / 2; circle.set({ radius: radius, left: origX - radius, top: origY - radius }); this.fabricCanvas.renderAll(); }); this.fabricCanvas.on('mouse:up', () => { isDown = false; this.saveAnnotation(); }); }, arrow: () => { this.fabricCanvas.isDrawingMode = false; this.fabricCanvas.selection = false; this.fabricCanvas.defaultCursor = 'crosshair'; let line, isDown, origX, origY; this.fabricCanvas.on('mouse:down', (options) => { isDown = true; const pointer = this.fabricCanvas.getPointer(options.e); origX = pointer.x; origY = pointer.y; line = new fabric.Line([origX, origY, origX, origY], { stroke: this.currentColor, strokeWidth: this.currentStrokeWidth, fill: this.currentColor, strokeLineCap: 'round', strokeLineJoin: 'round' }); this.fabricCanvas.add(line); }); this.fabricCanvas.on('mouse:move', (options) => { if (!isDown) return; const pointer = this.fabricCanvas.getPointer(options.e); line.set({ x2: pointer.x, y2: pointer.y }); // 添加箭头头部 this.addArrowHead(line, origX, origY, pointer.x, pointer.y); this.fabricCanvas.renderAll(); }); this.fabricCanvas.on('mouse:up', () => { isDown = false; this.saveAnnotation(); }); }, freedraw: () => { this.fabricCanvas.isDrawingMode = true; this.fabricCanvas.freeDrawingBrush = new fabric.PencilBrush(this.fabricCanvas); this.fabricCanvas.freeDrawingBrush.color = this.currentColor; this.fabricCanvas.freeDrawingBrush.width = this.currentStrokeWidth; this.fabricCanvas.selection = false; this.fabricCanvas.defaultCursor = 'crosshair'; this.fabricCanvas.on('path:created', () => { this.saveAnnotation(); }); } }; } addArrowHead(line, x1, y1, x2, y2) { // 移除旧的箭头头部 const objects = this.fabricCanvas.getObjects(); objects.forEach(obj => { if (obj.arrowHead) { this.fabricCanvas.remove(obj); } }); // 计算箭头角度 const angle = Math.atan2(y2 - y1, x2 - x1); const headLength = 15; // 创建箭头头部 const arrowHead = new fabric.Triangle({ left: x2, top: y2, angle: angle * 180 / Math.PI, fill: this.currentColor, width: headLength, height: headLength, originX: 'center', originY: 'center', arrowHead: true }); this.fabricCanvas.add(arrowHead); line.arrowHead
本文提供了一份详细的技术指南,介绍如何在WordPress平台中通过代码二次开发,集成一个在线简易PSD文件查看与标注工具。我们将从需求分析开始,逐步讲解技术选型、开发流程、核心功能实现以及优化建议,帮助开发者掌握在WordPress中扩展专业功能的方法。
- 引言:为什么在WordPress中集成PSD查看与标注工具
- 技术选型与准备工作
- WordPress插件架构设计
- 前端PSD查看器实现
- 标注功能开发
- 用户权限与文件管理
- 性能优化与安全考虑
- 测试与部署
- 扩展功能建议
- 结论
WordPress作为全球最流行的内容管理系统,不仅用于博客和网站建设,其强大的插件机制和可扩展性使其成为各种专业应用的理想平台。通过二次开发,我们可以将专业工具集成到WordPress中,为用户提供一体化的解决方案。
对于设计团队、客户协作和在线教育等场景,能够直接在网页中查看PSD文件并进行标注可以极大提高工作效率:
- 设计师与客户之间的设计评审
- 团队内部的设计协作
- 在线设计课程的素材展示
- 设计稿版本对比与反馈收集
虽然市场上有一些在线设计工具,但它们往往需要付费、功能过于复杂或无法与WordPress无缝集成。通过自主开发,我们可以创建轻量级、定制化的解决方案,完美融入现有WordPress环境。
在开始开发前,需要准备以下环境:
# 本地开发环境
- WordPress 5.8+ 安装
- PHP 7.4+ 环境
- MySQL 5.6+ 或 MariaDB 10.1+
- 代码编辑器(VS Code、PHPStorm等)
- 浏览器开发者工具
考虑到PSD文件的复杂性,我们需要选择合适的解析库:
- PSD.js - 基于JavaScript的PSD解析器,适合前端处理
- ImageMagick/GraphicsMagick - 服务器端处理方案
- Photoshop API - Adobe官方API(功能强大但成本较高)
对于简易查看器,我们推荐使用PSD.js,因为它:
- 纯前端实现,减轻服务器负担
- 开源免费,社区活跃
- 支持图层提取和基本信息读取
- Fabric.js - 强大的Canvas操作库
- Konva.js - 另一个优秀的Canvas库
- 自定义Canvas实现 - 更轻量但开发成本高
我们选择Fabric.js,因为它提供了丰富的图形对象和交互功能。
我们将采用标准的WordPress插件开发模式:
- 遵循WordPress编码标准
- 使用WordPress REST API进行前后端通信
- 利用WordPress的媒体库进行文件管理
创建插件基础目录结构:
wp-psd-viewer-annotator/
├── wp-psd-viewer-annotator.php # 主插件文件
├── includes/ # 核心功能文件
│ ├── class-psd-handler.php # PSD处理类
│ ├── class-annotation-manager.php # 标注管理类
│ └── class-file-manager.php # 文件管理类
├── admin/ # 后台管理文件
│ ├── css/
│ ├── js/
│ └── views/
├── public/ # 前端文件
│ ├── css/
│ ├── js/
│ └── views/
├── assets/ # 静态资源
│ ├── psd.js # PSD解析库
│ └── fabric.js # 标注库
└── languages/ # 国际化文件
<?php
/**
* Plugin Name: PSD Viewer & Annotator for WordPress
* Plugin URI: https://yourwebsite.com/
* Description: 在WordPress中查看和标注PSD文件的工具
* Version: 1.0.0
* Author: Your Name
* License: GPL v2 or later
*/
// 防止直接访问
if (!defined('ABSPATH')) {
exit;
}
// 定义插件常量
define('PSD_VA_VERSION', '1.0.0');
define('PSD_VA_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('PSD_VA_PLUGIN_URL', plugin_dir_url(__FILE__));
define('PSD_VA_MAX_FILE_SIZE', 104857600); // 100MB
// 自动加载类文件
spl_autoload_register(function ($class_name) {
$prefix = 'PSD_VA_';
$base_dir = PSD_VA_PLUGIN_DIR . 'includes/';
if (strpos($class_name, $prefix) !== 0) {
return;
}
$relative_class = substr($class_name, strlen($prefix));
$file = $base_dir . 'class-' . strtolower(str_replace('_', '-', $relative_class)) . '.php';
if (file_exists($file)) {
require_once $file;
}
});
// 初始化插件
function psd_va_init() {
// 检查依赖
if (!function_exists('gd_info')) {
add_action('admin_notices', function() {
echo '<div class="notice notice-error"><p>PSD查看器需要GD库支持,请启用PHP的GD扩展。</p></div>';
});
return;
}
// 初始化核心类
$psd_handler = new PSD_VA_PSD_Handler();
$annotation_manager = new PSD_VA_Annotation_Manager();
$file_manager = new PSD_VA_File_Manager();
// 注册短代码
add_shortcode('psd_viewer', array($psd_handler, 'shortcode_handler'));
// 注册REST API端点
add_action('rest_api_init', array($annotation_manager, 'register_rest_routes'));
// 注册管理菜单
add_action('admin_menu', 'psd_va_admin_menu');
}
add_action('plugins_loaded', 'psd_va_init');
// 管理菜单
function psd_va_admin_menu() {
add_menu_page(
'PSD查看器',
'PSD查看器',
'manage_options',
'psd-viewer',
'psd_va_admin_page',
'dashicons-format-image',
30
);
}
function psd_va_admin_page() {
include PSD_VA_PLUGIN_DIR . 'admin/views/admin-page.php';
}
// 激活/停用钩子
register_activation_hook(__FILE__, 'psd_va_activate');
register_deactivation_hook(__FILE__, 'psd_va_deactivate');
function psd_va_activate() {
// 创建必要的数据库表
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$annotations_table = $wpdb->prefix . 'psd_va_annotations';
$sql = "CREATE TABLE IF NOT EXISTS $annotations_table (
id bigint(20) NOT NULL AUTO_INCREMENT,
psd_id bigint(20) NOT NULL,
user_id bigint(20) NOT NULL,
annotation_data longtext NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY psd_id (psd_id),
KEY user_id (user_id)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
// 设置默认选项
add_option('psd_va_max_file_size', PSD_VA_MAX_FILE_SIZE);
add_option('psd_va_allowed_roles', array('administrator', 'editor', 'author'));
}
function psd_va_deactivate() {
// 清理临时文件
$upload_dir = wp_upload_dir();
$temp_dir = $upload_dir['basedir'] . '/psd-va-temp/';
if (is_dir($temp_dir)) {
array_map('unlink', glob($temp_dir . '*'));
rmdir($temp_dir);
}
}
<?php
/**
* Plugin Name: PSD Viewer & Annotator for WordPress
* Plugin URI: https://yourwebsite.com/
* Description: 在WordPress中查看和标注PSD文件的工具
* Version: 1.0.0
* Author: Your Name
* License: GPL v2 or later
*/
// 防止直接访问
if (!defined('ABSPATH')) {
exit;
}
// 定义插件常量
define('PSD_VA_VERSION', '1.0.0');
define('PSD_VA_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('PSD_VA_PLUGIN_URL', plugin_dir_url(__FILE__));
define('PSD_VA_MAX_FILE_SIZE', 104857600); // 100MB
// 自动加载类文件
spl_autoload_register(function ($class_name) {
$prefix = 'PSD_VA_';
$base_dir = PSD_VA_PLUGIN_DIR . 'includes/';
if (strpos($class_name, $prefix) !== 0) {
return;
}
$relative_class = substr($class_name, strlen($prefix));
$file = $base_dir . 'class-' . strtolower(str_replace('_', '-', $relative_class)) . '.php';
if (file_exists($file)) {
require_once $file;
}
});
// 初始化插件
function psd_va_init() {
// 检查依赖
if (!function_exists('gd_info')) {
add_action('admin_notices', function() {
echo '<div class="notice notice-error"><p>PSD查看器需要GD库支持,请启用PHP的GD扩展。</p></div>';
});
return;
}
// 初始化核心类
$psd_handler = new PSD_VA_PSD_Handler();
$annotation_manager = new PSD_VA_Annotation_Manager();
$file_manager = new PSD_VA_File_Manager();
// 注册短代码
add_shortcode('psd_viewer', array($psd_handler, 'shortcode_handler'));
// 注册REST API端点
add_action('rest_api_init', array($annotation_manager, 'register_rest_routes'));
// 注册管理菜单
add_action('admin_menu', 'psd_va_admin_menu');
}
add_action('plugins_loaded', 'psd_va_init');
// 管理菜单
function psd_va_admin_menu() {
add_menu_page(
'PSD查看器',
'PSD查看器',
'manage_options',
'psd-viewer',
'psd_va_admin_page',
'dashicons-format-image',
30
);
}
function psd_va_admin_page() {
include PSD_VA_PLUGIN_DIR . 'admin/views/admin-page.php';
}
// 激活/停用钩子
register_activation_hook(__FILE__, 'psd_va_activate');
register_deactivation_hook(__FILE__, 'psd_va_deactivate');
function psd_va_activate() {
// 创建必要的数据库表
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$annotations_table = $wpdb->prefix . 'psd_va_annotations';
$sql = "CREATE TABLE IF NOT EXISTS $annotations_table (
id bigint(20) NOT NULL AUTO_INCREMENT,
psd_id bigint(20) NOT NULL,
user_id bigint(20) NOT NULL,
annotation_data longtext NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY psd_id (psd_id),
KEY user_id (user_id)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
// 设置默认选项
add_option('psd_va_max_file_size', PSD_VA_MAX_FILE_SIZE);
add_option('psd_va_allowed_roles', array('administrator', 'editor', 'author'));
}
function psd_va_deactivate() {
// 清理临时文件
$upload_dir = wp_upload_dir();
$temp_dir = $upload_dir['basedir'] . '/psd-va-temp/';
if (is_dir($temp_dir)) {
array_map('unlink', glob($temp_dir . '*'));
rmdir($temp_dir);
}
}
我们需要创建以下数据库表来存储标注信息:
-- 标注数据表
CREATE TABLE wp_psd_va_annotations (
id BIGINT(20) NOT NULL AUTO_INCREMENT,
psd_id BIGINT(20) NOT NULL, -- 关联的PSD文件ID
user_id BIGINT(20) NOT NULL, -- 创建标注的用户ID
annotation_data LONGTEXT NOT NULL, -- 标注数据(JSON格式)
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
INDEX psd_id_idx (psd_id),
INDEX user_id_idx (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
// 在插件中注册脚本
function psd_va_enqueue_scripts() {
// 前端样式
wp_enqueue_style(
'psd-va-frontend',
PSD_VA_PLUGIN_URL . 'public/css/frontend.css',
array(),
PSD_VA_VERSION
);
// 核心库
wp_enqueue_script(
'psd-js',
PSD_VA_PLUGIN_URL . 'assets/js/psd.min.js',
array(),
'0.8.0',
true
);
wp_enqueue_script(
'fabric-js',
PSD_VA_PLUGIN_URL . 'assets/js/fabric.min.js',
array(),
'4.5.0',
true
);
// 主脚本
wp_enqueue_script(
'psd-va-main',
PSD_VA_PLUGIN_URL . 'public/js/main.js',
array('jquery', 'psd-js', 'fabric-js'),
PSD_VA_VERSION,
true
);
// 本地化脚本
wp_localize_script('psd-va-main', 'psd_va_ajax', array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('psd_va_nonce'),
'rest_url' => rest_url('psd-va/v1/'),
'max_file_size' => get_option('psd_va_max_file_size', PSD_VA_MAX_FILE_SIZE)
));
}
add_action('wp_enqueue_scripts', 'psd_va_enqueue_scripts');
// 在插件中注册脚本
function psd_va_enqueue_scripts() {
// 前端样式
wp_enqueue_style(
'psd-va-frontend',
PSD_VA_PLUGIN_URL . 'public/css/frontend.css',
array(),
PSD_VA_VERSION
);
// 核心库
wp_enqueue_script(
'psd-js',
PSD_VA_PLUGIN_URL . 'assets/js/psd.min.js',
array(),
'0.8.0',
true
);
wp_enqueue_script(
'fabric-js',
PSD_VA_PLUGIN_URL . 'assets/js/fabric.min.js',
array(),
'4.5.0',
true
);
// 主脚本
wp_enqueue_script(
'psd-va-main',
PSD_VA_PLUGIN_URL . 'public/js/main.js',
array('jquery', 'psd-js', 'fabric-js'),
PSD_VA_VERSION,
true
);
// 本地化脚本
wp_localize_script('psd-va-main', 'psd_va_ajax', array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('psd_va_nonce'),
'rest_url' => rest_url('psd-va/v1/'),
'max_file_size' => get_option('psd_va_max_file_size', PSD_VA_MAX_FILE_SIZE)
));
}
add_action('wp_enqueue_scripts', 'psd_va_enqueue_scripts');
// public/js/main.js - PSD查看器核心功能
class PSDViewer {
constructor(containerId, options = {}) {
this.container = document.getElementById(containerId);
this.options = Object.assign({
psdUrl: '',
width: 800,
height: 600,
showLayers: true,
allowDownload: true
}, options);
this.canvas = null;
this.psd = null;
this.layers = [];
this.currentScale = 1;
this.init();
}
async init() {
// 创建UI结构
this.createUI();
// 加载PSD文件
if (this.options.psdUrl) {
await this.loadPSD(this.options.psdUrl);
}
}
createUI() {
// 创建主容器
this.container.innerHTML = `
<div class="psd-viewer-container">
<div class="psd-toolbar">
<button class="tool-btn zoom-in" title="放大">+</button>
<button class="tool-btn zoom-out" title="缩小">-</button>
<button class="tool-btn reset-zoom" title="重置缩放">1:1</button>
<span class="zoom-level">100%</span>
<div class="tool-separator"></div>
<button class="tool-btn toggle-layers" title="显示/隐藏图层">图层</button>
<button class="tool-btn download-image" title="下载为PNG">下载</button>
</div>
<div class="psd-main-area">
<div class="psd-canvas-container">
<canvas id="psd-canvas-${this.container.id}"></canvas>
</div>
<div class="psd-layers-panel">
<h3>图层</h3>
<div class="layers-list"></div>
</div>
</div>
<div class="psd-status-bar">
<span class="file-info"></span>
<span class="canvas-size"></span>
</div>
</div>
`;
// 获取Canvas元素
this.canvas = document.getElementById(`psd-canvas-${this.container.id}`);
this.ctx = this.canvas.getContext('2d');
// 绑定事件
this.bindEvents();
}
async loadPSD(url) {
try {
// 显示加载状态
this.showLoading();
// 获取PSD文件
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
// 解析PSD
this.psd = PSD.fromArrayBuffer(arrayBuffer);
this.psd.parse();
// 渲染PSD
this.renderPSD();
// 提取图层信息
this.extractLayers();
// 更新UI
this.updateFileInfo();
} catch (error) {
console.error('加载PSD失败:', error);
this.showError('无法加载PSD文件: ' + error.message);
}
}
renderPSD() {
if (!this.psd) return;
// 获取PSD尺寸
const width = this.psd.header.width;
const height = this.psd.header.height;
// 设置Canvas尺寸
this.canvas.width = width;
this.canvas.height = height;
// 渲染到Canvas
const imageData = this.psd.image.toCanvas();
this.ctx.drawImage(imageData, 0, 0);
// 更新Canvas显示尺寸
this.fitToContainer();
}
extractLayers() {
if (!this.psd || !this.options.showLayers) return;
this.layers = [];
const extractLayerInfo = (layer, depth = 0) => {
if (layer.visible === false) return;
const layerInfo = {
id: layer.id || Math.random().toString(36).substr(2, 9),
name: layer.name || '未命名图层',
visible: layer.visible,
opacity: layer.opacity,
depth: depth,
children: []
};
if (layer.children && layer.children.length > 0) {
layer.children.forEach(child => {
extractLayerInfo(child, depth + 1);
});
}
this.layers.push(layerInfo);
};
extractLayerInfo(this.psd.tree());
this.renderLayersList();
}
renderLayersList() {
const layersList = this.container.querySelector('.layers-list');
layersList.innerHTML = '';
this.layers.forEach(layer => {
const layerItem = document.createElement('div');
layerItem.className = 'layer-item';
layerItem.style.paddingLeft = (layer.depth * 20) + 'px';
layerItem.innerHTML = `
<label>
<input type="checkbox" ${layer.visible ? 'checked' : ''}
data-layer-id="${layer.id}">
${layer.name}
</label>
`;
layersList.appendChild(layerItem);
});
}
fitToContainer() {
const container = this.canvas.parentElement;
const containerWidth = container.clientWidth;
const containerHeight = container.clientHeight;
const psdWidth = this.canvas.width;
const psdHeight = this.canvas.height;
// 计算适合容器的缩放比例
const scaleX = containerWidth / psdWidth;
const scaleY = containerHeight / psdHeight;
this.currentScale = Math.min(scaleX, scaleY, 1);
// 应用缩放
this.canvas.style.width = (psdWidth * this.currentScale) + 'px';
this.canvas.style.height = (psdHeight * this.currentScale) + 'px';
// 更新缩放显示
this.updateZoomDisplay();
}
updateZoomDisplay() {
const zoomElement = this.container.querySelector('.zoom-level');
if (zoomElement) {
zoomElement.textContent = Math.round(this.currentScale * 100) + '%';
}
}
bindEvents() {
// 缩放按钮
this.container.querySelector('.zoom-in').addEventListener('click', () => {
this.zoom(0.1);
});
this.container.querySelector('.zoom-out').addEventListener('click', () => {
this.zoom(-0.1);
});
this.container.querySelector('.reset-zoom').addEventListener('click', () => {
this.currentScale = 1;
this.canvas.style.width = this.canvas.width + 'px';
// public/js/main.js - PSD查看器核心功能
class PSDViewer {
constructor(containerId, options = {}) {
this.container = document.getElementById(containerId);
this.options = Object.assign({
psdUrl: '',
width: 800,
height: 600,
showLayers: true,
allowDownload: true
}, options);
this.canvas = null;
this.psd = null;
this.layers = [];
this.currentScale = 1;
this.init();
}
async init() {
// 创建UI结构
this.createUI();
// 加载PSD文件
if (this.options.psdUrl) {
await this.loadPSD(this.options.psdUrl);
}
}
createUI() {
// 创建主容器
this.container.innerHTML = `
<div class="psd-viewer-container">
<div class="psd-toolbar">
<button class="tool-btn zoom-in" title="放大">+</button>
<button class="tool-btn zoom-out" title="缩小">-</button>
<button class="tool-btn reset-zoom" title="重置缩放">1:1</button>
<span class="zoom-level">100%</span>
<div class="tool-separator"></div>
<button class="tool-btn toggle-layers" title="显示/隐藏图层">图层</button>
<button class="tool-btn download-image" title="下载为PNG">下载</button>
</div>
<div class="psd-main-area">
<div class="psd-canvas-container">
<canvas id="psd-canvas-${this.container.id}"></canvas>
</div>
<div class="psd-layers-panel">
<h3>图层</h3>
<div class="layers-list"></div>
</div>
</div>
<div class="psd-status-bar">
<span class="file-info"></span>
<span class="canvas-size"></span>
</div>
</div>
`;
// 获取Canvas元素
this.canvas = document.getElementById(`psd-canvas-${this.container.id}`);
this.ctx = this.canvas.getContext('2d');
// 绑定事件
this.bindEvents();
}
async loadPSD(url) {
try {
// 显示加载状态
this.showLoading();
// 获取PSD文件
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
// 解析PSD
this.psd = PSD.fromArrayBuffer(arrayBuffer);
this.psd.parse();
// 渲染PSD
this.renderPSD();
// 提取图层信息
this.extractLayers();
// 更新UI
this.updateFileInfo();
} catch (error) {
console.error('加载PSD失败:', error);
this.showError('无法加载PSD文件: ' + error.message);
}
}
renderPSD() {
if (!this.psd) return;
// 获取PSD尺寸
const width = this.psd.header.width;
const height = this.psd.header.height;
// 设置Canvas尺寸
this.canvas.width = width;
this.canvas.height = height;
// 渲染到Canvas
const imageData = this.psd.image.toCanvas();
this.ctx.drawImage(imageData, 0, 0);
// 更新Canvas显示尺寸
this.fitToContainer();
}
extractLayers() {
if (!this.psd || !this.options.showLayers) return;
this.layers = [];
const extractLayerInfo = (layer, depth = 0) => {
if (layer.visible === false) return;
const layerInfo = {
id: layer.id || Math.random().toString(36).substr(2, 9),
name: layer.name || '未命名图层',
visible: layer.visible,
opacity: layer.opacity,
depth: depth,
children: []
};
if (layer.children && layer.children.length > 0) {
layer.children.forEach(child => {
extractLayerInfo(child, depth + 1);
});
}
this.layers.push(layerInfo);
};
extractLayerInfo(this.psd.tree());
this.renderLayersList();
}
renderLayersList() {
const layersList = this.container.querySelector('.layers-list');
layersList.innerHTML = '';
this.layers.forEach(layer => {
const layerItem = document.createElement('div');
layerItem.className = 'layer-item';
layerItem.style.paddingLeft = (layer.depth * 20) + 'px';
layerItem.innerHTML = `
<label>
<input type="checkbox" ${layer.visible ? 'checked' : ''}
data-layer-id="${layer.id}">
${layer.name}
</label>
`;
layersList.appendChild(layerItem);
});
}
fitToContainer() {
const container = this.canvas.parentElement;
const containerWidth = container.clientWidth;
const containerHeight = container.clientHeight;
const psdWidth = this.canvas.width;
const psdHeight = this.canvas.height;
// 计算适合容器的缩放比例
const scaleX = containerWidth / psdWidth;
const scaleY = containerHeight / psdHeight;
this.currentScale = Math.min(scaleX, scaleY, 1);
// 应用缩放
this.canvas.style.width = (psdWidth * this.currentScale) + 'px';
this.canvas.style.height = (psdHeight * this.currentScale) + 'px';
// 更新缩放显示
this.updateZoomDisplay();
}
updateZoomDisplay() {
const zoomElement = this.container.querySelector('.zoom-level');
if (zoomElement) {
zoomElement.textContent = Math.round(this.currentScale * 100) + '%';
}
}
bindEvents() {
// 缩放按钮
this.container.querySelector('.zoom-in').addEventListener('click', () => {
this.zoom(0.1);
});
this.container.querySelector('.zoom-out').addEventListener('click', () => {
this.zoom(-0.1);
});
this.container.querySelector('.reset-zoom').addEventListener('click', () => {
this.currentScale = 1;
this.canvas.style.width = this.canvas.width + 'px';
.style.height = this.canvas.height + 'px';
this.updateZoomDisplay();
});
// 图层显示/隐藏
this.container.querySelector('.toggle-layers').addEventListener('click', () => {
const panel = this.container.querySelector('.psd-layers-panel');
panel.classList.toggle('collapsed');
});
// 下载功能
this.container.querySelector('.download-image').addEventListener('click', () => {
this.downloadAsPNG();
});
// 图层复选框事件委托
this.container.querySelector('.layers-list').addEventListener('change', (e) => {
if (e.target.type === 'checkbox') {
const layerId = e.target.dataset.layerId;
this.toggleLayerVisibility(layerId, e.target.checked);
}
});
// Canvas拖拽和缩放
this.setupCanvasInteractions();
}
zoom(delta) {
this.currentScale = Math.max(0.1, Math.min(5, this.currentScale + delta));
this.canvas.style.width = (this.canvas.width * this.currentScale) + 'px';
this.canvas.style.height = (this.canvas.height * this.currentScale) + 'px';
this.updateZoomDisplay();
}
downloadAsPNG() {
const link = document.createElement('a');
link.download = 'psd-export.png';
link.href = this.canvas.toDataURL('image/png');
link.click();
}
toggleLayerVisibility(layerId, visible) {
// 这里可以实现图层显示/隐藏逻辑
console.log(`图层 ${layerId} 可见性: ${visible}`);
// 实际实现需要重新渲染PSD并隐藏/显示特定图层
}
setupCanvasInteractions() {
let isDragging = false;
let lastX = 0;
let lastY = 0;
this.canvas.addEventListener('mousedown', (e) => {
isDragging = true;
lastX = e.clientX;
lastY = e.clientY;
this.canvas.style.cursor = 'grabbing';
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const deltaX = e.clientX - lastX;
const deltaY = e.clientY - lastY;
// 更新Canvas位置
const currentLeft = parseInt(this.canvas.style.left || 0);
const currentTop = parseInt(this.canvas.style.top || 0);
this.canvas.style.left = (currentLeft + deltaX) + 'px';
this.canvas.style.top = (currentTop + deltaY) + 'px';
lastX = e.clientX;
lastY = e.clientY;
});
document.addEventListener('mouseup', () => {
isDragging = false;
this.canvas.style.cursor = 'grab';
});
// 鼠标滚轮缩放
this.canvas.addEventListener('wheel', (e) => {
e.preventDefault();
const delta = e.deltaY > 0 ? -0.1 : 0.1;
this.zoom(delta);
});
}
showLoading() {
this.container.querySelector('.psd-canvas-container').innerHTML = `
<div class="loading-spinner">
<div class="spinner"></div>
<p>加载PSD文件中...</p>
</div>
`;
}
showError(message) {
this.container.querySelector('.psd-canvas-container').innerHTML = `
<div class="error-message">
<p>${message}</p>
<button class="retry-btn">重试</button>
</div>
`;
// 重试按钮事件
this.container.querySelector('.retry-btn').addEventListener('click', () => {
this.loadPSD(this.options.psdUrl);
});
}
updateFileInfo() {
if (!this.psd) return;
const fileInfo = this.container.querySelector('.file-info');
const canvasSize = this.container.querySelector('.canvas-size');
if (fileInfo) {
fileInfo.textContent = `尺寸: ${this.psd.header.width} × ${this.psd.header.height} 像素 | 颜色模式: ${this.psd.header.mode}`;
}
if (canvasSize) {
canvasSize.textContent = `缩放: ${Math.round(this.currentScale * 100)}%`;
}
}
}
// 初始化查看器
document.addEventListener('DOMContentLoaded', function() {
const psdContainers = document.querySelectorAll('.psd-viewer');
psdContainers.forEach(container => {
const psdUrl = container.dataset.psdUrl;
const options = {
psdUrl: psdUrl,
showLayers: container.dataset.showLayers !== 'false',
allowDownload: container.dataset.allowDownload !== 'false'
};
new PSDViewer(container.id, options);
});
});
### 4.3 前端样式设计
/ public/css/frontend.css /
.psd-viewer-container {
width: 100%;
height: 600px;
border: 1px solid #ddd;
border-radius: 4px;
overflow: hidden;
display: flex;
flex-direction: column;
background: #f5f5f5;
}
.psd-toolbar {
background: #fff;
border-bottom: 1px solid #ddd;
padding: 10px;
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.tool-btn {
padding: 6px 12px;
background: #f0f0f0;
border: 1px solid #ccc;
border-radius: 3px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.tool-btn:hover {
background: #e0e0e0;
border-color: #999;
}
.tool-separator {
width: 1px;
height: 20px;
background: #ddd;
margin: 0 10px;
}
.zoom-level {
font-size: 14px;
color: #666;
min-width: 50px;
}
.psd-main-area {
flex: 1;
display: flex;
overflow: hidden;
}
.psd-canvas-container {
flex: 1;
position: relative;
overflow: auto;
background:
linear-gradient(45deg, #eee 25%, transparent 25%),
linear-gradient(-45deg, #eee 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #eee 75%),
linear-gradient(-45deg, transparent 75%, #eee 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
}
display: block;
position: absolute;
top: 0;
left: 0;
cursor: grab;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
display: block;
position: absolute;
top: 0;
left: 0;
cursor: grab;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.psd-layers-panel {
width: 250px;
background: #fff;
border-left: 1px solid #ddd;
padding: 15px;
overflow-y: auto;
transition: width 0.3s;
}
.psd-layers-panel.collapsed {
width: 0;
padding: 0;
border: none;
overflow: hidden;
}
.psd-layers-panel h3 {
margin-top: 0;
margin-bottom: 15px;
font-size: 16px;
color: #333;
}
.layers-list {
max-height: 400px;
overflow-y: auto;
}
.layer-item {
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.layer-item label {
display: flex;
align-items: center;
cursor: pointer;
font-size: 14px;
}
.layer-item input[type="checkbox"] {
margin-right: 8px;
}
.psd-status-bar {
background: #fff;
border-top: 1px solid #ddd;
padding: 8px 15px;
display: flex;
justify-content: space-between;
font-size: 12px;
color: #666;
flex-shrink: 0;
}
.loading-spinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid #f3f3f3;
border-top: 3px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: #e74c3c;
}
.retry-btn {
margin-top: 10px;
padding: 8px 16px;
background: #3498db;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
}
/ 响应式设计 /
@media (max-width: 768px) {
.psd-viewer-container {
height: 400px;
}
.psd-layers-panel {
position: absolute;
right: 0;
top: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.95);
z-index: 100;
}
.psd-toolbar {
flex-wrap: wrap;
gap: 5px;
}
}
## 5. 标注功能开发
### 5.1 标注工具类实现
// public/js/annotation.js
class PSDAnnotator {
constructor(canvasElement, options = {}) {
this.canvas = canvasElement;
this.fabricCanvas = null;
this.annotations = [];
this.currentTool = 'select';
this.currentColor = '#ff0000';
this.currentStrokeWidth = 2;
this.options = Object.assign({
enableText: true,
enableArrow: true,
enableRectangle: true,
enableCircle: true,
enableFreeDraw: true
}, options);
this.initFabricCanvas();
this.setupAnnotationTools();
}
initFabricCanvas() {
// 创建Fabric.js Canvas
this.fabricCanvas = new fabric.Canvas(this.canvas, {
selection: true,
preserveObjectStacking: true,
backgroundColor: 'transparent'
});
// 设置Canvas尺寸与底层PSD Canvas一致
const psdCanvas = document.getElementById(this.canvas.id.replace('annotation-', ''));
if (psdCanvas) {
this.fabricCanvas.setWidth(psdCanvas.width);
this.fabricCanvas.setHeight(psdCanvas.height);
this.fabricCanvas.setDimensions({
width: psdCanvas.style.width,
height: psdCanvas.style.height
});
}
// 绑定事件
this.bindCanvasEvents();
}
setupAnnotationTools() {
this.tools = {
select: () => {
this.fabricCanvas.isDrawingMode = false;
this.fabricCanvas.selection = true;
this.fabricCanvas.defaultCursor = 'default';
},
text: () => {
this.fabricCanvas.isDrawingMode = false;
this.fabricCanvas.selection = false;
this.fabricCanvas.defaultCursor = 'text';
this.fabricCanvas.on('mouse:down', (options) => {
if (options.target) return;
const point = this.fabricCanvas.getPointer(options.e);
const text = new fabric.IText('输入文字', {
left: point.x,
top: point.y,
fontSize: 16,
fill: this.currentColor,
fontFamily: 'Arial'
});
this.fabricCanvas.add(text);
this.fabricCanvas.setActiveObject(text);
text.enterEditing();
text.selectAll();
});
},
rectangle: () => {
this.fabricCanvas.isDrawingMode = false;
this.fabricCanvas.selection = false;
this.fabricCanvas.defaultCursor = 'crosshair';
let rect, isDown, origX, origY;
this.fabricCanvas.on('mouse:down', (options) => {
isDown = true;
const pointer = this.fabricCanvas.getPointer(options.e);
origX = pointer.x;
origY = pointer.y;
rect = new fabric.Rect({
left: origX,
top: origY,
width: 0,
height: 0,
fill: 'transparent',
stroke: this.currentColor,
strokeWidth: this.currentStrokeWidth
});
this.fabricCanvas.add(rect);
});
this.fabricCanvas.on('mouse:move', (options) => {
if (!isDown) return;
const pointer = this.fabricCanvas.getPointer(options.e);
if (origX > pointer.x) {
rect.set({ left: pointer.x });
}
if (origY > pointer.y) {
rect.set({ top: pointer.y });
}
rect.set({
width: Math.abs(origX - pointer.x),
height: Math.abs(origY - pointer.y)
});
this.fabricCanvas.renderAll();
});
this.fabricCanvas.on('mouse:up', () => {
isDown = false;
this.saveAnnotation();
});
},
circle: () => {
this.fabricCanvas.isDrawingMode = false;
this.fabricCanvas.selection = false;
this.fabricCanvas.defaultCursor = 'crosshair';
let circle, isDown, origX, origY;
this.fabricCanvas.on('mouse:down', (options) => {
isDown = true;
const pointer = this.fabricCanvas.getPointer(options.e);
origX = pointer.x;
origY = pointer.y;
circle = new fabric.Circle({
left: origX,
top: origY,
radius: 0,
fill: 'transparent',
stroke: this.currentColor,
strokeWidth: this.currentStrokeWidth
});
this.fabricCanvas.add(circle);
});
this.fabricCanvas.on('mouse:move', (options) => {
if (!isDown) return;
const pointer = this.fabricCanvas.getPointer(options.e);
const radius = Math.sqrt(
Math.pow(origX - pointer.x, 2) +
Math.pow(origY - pointer.y, 2)
) / 2;
circle.set({
radius: radius,
left: origX - radius,
top: origY - radius
});
this.fabricCanvas.renderAll();
});
this.fabricCanvas.on('mouse:up', () => {
isDown = false;
this.saveAnnotation();
});
},
arrow: () => {
this.fabricCanvas.isDrawingMode = false;
this.fabricCanvas.selection = false;
this.fabricCanvas.defaultCursor = 'crosshair';
let line, isDown, origX, origY;
this.fabricCanvas.on('mouse:down', (options) => {
isDown = true;
const pointer = this.fabricCanvas.getPointer(options.e);
origX = pointer.x;
origY = pointer.y;
line = new fabric.Line([origX, origY, origX, origY], {
stroke: this.currentColor,
strokeWidth: this.currentStrokeWidth,
fill: this.currentColor,
strokeLineCap: 'round',
strokeLineJoin: 'round'
});
this.fabricCanvas.add(line);
});
this.fabricCanvas.on('mouse:move', (options) => {
if (!isDown) return;
const pointer = this.fabricCanvas.getPointer(options.e);
line.set({ x2: pointer.x, y2: pointer.y });
// 添加箭头头部
this.addArrowHead(line, origX, origY, pointer.x, pointer.y);
this.fabricCanvas.renderAll();
});
this.fabricCanvas.on('mouse:up', () => {
isDown = false;
this.saveAnnotation();
});
},
freedraw: () => {
this.fabricCanvas.isDrawingMode = true;
this.fabricCanvas.freeDrawingBrush = new fabric.PencilBrush(this.fabricCanvas);
this.fabricCanvas.freeDrawingBrush.color = this.currentColor;
this.fabricCanvas.freeDrawingBrush.width = this.currentStrokeWidth;
this.fabricCanvas.selection = false;
this.fabricCanvas.defaultCursor = 'crosshair';
this.fabricCanvas.on('path:created', () => {
this.saveAnnotation();
});
}
};
}
addArrowHead(line, x1, y1, x2, y2) {
// 移除旧的箭头头部
const objects = this.fabricCanvas.getObjects();
objects.forEach(obj => {
if (obj.arrowHead) {
this.fabricCanvas.remove(obj);
}
});
// 计算箭头角度
const angle = Math.atan2(y2 - y1, x2 - x1);
const headLength = 15;
// 创建箭头头部
const arrowHead = new fabric.Triangle({
left: x2,
top: y2,
angle: angle * 180 / Math.PI,
fill: this.currentColor,
width: headLength,
height: headLength,
originX: 'center',
originY: 'center',
arrowHead: true
});
this.fabricCanvas.add(arrowHead);
line.arrowHead


