跳至内容

分类: 应用软件

WordPress开发教程,集成网站用户贡献内容积分兑换商城系统

WordPress开发教程:集成用户贡献内容积分兑换商城系统与常用互联网小工具功能 引言:WordPress的无限可能性 在当今数字化时代,网站已不仅仅是信息展示的平台,更是用户互动和参与的重要场所。WordPress作为全球最受欢迎的内容管理系统,其真正的强大之处在于其高度的可扩展性和灵活性。本教程将深入探讨如何通过WordPress代码二次开发,实现一个集用户贡献内容、积分系统和兑换商城于一体的综合性平台,同时集成多种实用互联网小工具功能。 通过本教程,您将学习到如何将WordPress从一个简单的博客平台转变为一个功能丰富的互动社区和电子商务系统。无论您是WordPress开发者、网站管理员还是希望扩展网站功能的创业者,这些知识都将为您打开新的可能性。 第一部分:项目规划与环境搭建 1.1 系统需求分析与功能规划 在开始开发之前,我们需要明确项目的核心需求: 用户贡献内容系统:允许用户提交文章、评论、图片等内容 积分管理系统:根据用户行为自动计算和分配积分 积分兑换商城:用户可以使用积分兑换实物或虚拟商品 常用互联网小工具:集成实用工具如短链接生成、二维码创建等 技术架构规划: 主框架:WordPress 5.8+ 开发方式:自定义插件+主题二次开发 数据库:MySQL 5.6+ 前端技术:HTML5, CSS3, JavaScript (jQuery/Vue.js) 安全考虑:数据验证、SQL注入防护、XSS防护 1.2 开发环境配置 首先,确保您的开发环境满足以下要求: // 检查WordPress环境要求 define('WP_DEBUG', true); // 开发阶段开启调试模式 define('WP_DEBUG_LOG', true); // 将错误记录到日志 define('WP_DEBUG_DISPLAY', false); // 不直接显示错误 // 推荐服务器配置 // PHP版本:7.4或更高 // MySQL版本:5.6或更高 // 内存限制:至少256MB 安装必要的开发工具: 本地服务器环境(XAMPP、MAMP或Local by Flywheel) 代码编辑器(VS Code、PHPStorm等) Git版本控制系统 浏览器开发者工具 1.3 创建自定义插件基础结构 我们将创建一个主插件来管理所有功能: /* Plugin Name: 用户积分商城系统 Plugin URI: https://yourwebsite.com/ Description: 集成用户贡献内容、积分系统和兑换商城的综合解决方案 Version: 1.0.0 Author: 您的名称 License: GPL v2 or later Text Domain: user-points-mall */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('UPM_PLUGIN_PATH', plugin_dir_path(__FILE__)); define('UPM_PLUGIN_URL', plugin_dir_url(__FILE__)); define('UPM_VERSION', '1.0.0'); // 初始化插件 function upm_init() { // 加载语言文件 load_plugin_textdomain('user-points-mall', false, dirname(plugin_basename(__FILE__)) . '/languages'); // 检查依赖 upm_check_dependencies(); } add_action('plugins_loaded', 'upm_init'); // 检查系统依赖 function upm_check_dependencies() { $errors = array(); // 检查PHP版本 if (version_compare(PHP_VERSION, '7.4', '<')) { $errors[] = __('需要PHP 7.4或更高版本', 'user-points-mall'); } // 检查WordPress版本 if (version_compare(get_bloginfo('version'), '5.8', '<')) { $errors[] = __('需要WordPress 5.8或更高版本', 'user-points-mall'); } // 如果有错误,显示通知 if (!empty($errors)) { add_action('admin_notices', function() use ($errors) { echo '<div class="notice notice-error"><p>'; echo __('用户积分商城系统插件无法激活:', 'user-points-mall'); echo '<ul>'; foreach ($errors as $error) { echo '<li>' . $error . '</li>'; } echo '</ul></p></div>'; }); // 停用插件 deactivate_plugins(plugin_basename(__FILE__)); } } 第二部分:用户贡献内容系统开发 2.1 自定义文章类型与用户提交表单 创建用户贡献内容系统,首先需要定义自定义文章类型: // 注册用户贡献内容类型 function upm_register_contribution_type() { $labels = array( 'name' => __('用户贡献', 'user-points-mall'), 'singular_name' => __('贡献内容', 'user-points-mall'), 'menu_name' => __('用户贡献', 'user-points-mall'), 'add_new' => __('添加新贡献', 'user-points-mall'), 'add_new_item' => __('添加新贡献内容', 'user-points-mall'), 'edit_item' => __('编辑贡献内容', 'user-points-mall'), 'new_item' => __('新贡献内容', 'user-points-mall'), 'view_item' => __('查看贡献内容', 'user-points-mall'), 'search_items' => __('搜索贡献内容', 'user-points-mall'), 'not_found' => __('未找到贡献内容', 'user-points-mall'), 'not_found_in_trash' => __('回收站中无贡献内容', 'user-points-mall'), ); $args = array( 'labels' => $labels, 'public' => true, 'publicly_queryable' => true, 'show_ui' => true, 'show_in_menu' => true, 'query_var' => true, 'rewrite' => array('slug' => 'contribution'), 'capability_type' => 'post', 'has_archive' => true, 'hierarchical' => false, 'menu_position' => 5, 'menu_icon' => 'dashicons-groups', 'supports' => array('title', 'editor', 'author', 'thumbnail', 'excerpt', 'comments'), 'show_in_rest' => true, // 支持Gutenberg编辑器 ); register_post_type('user_contribution', $args); } add_action('init', 'upm_register_contribution_type'); // 添加贡献状态分类 function upm_register_contribution_status() { $labels = array( 'name' => __('贡献状态', 'user-points-mall'), 'singular_name' => __('状态', 'user-points-mall'), 'search_items' => __('搜索状态', 'user-points-mall'), 'all_items' => __('所有状态', 'user-points-mall'), 'parent_item' => __('父状态', 'user-points-mall'), 'parent_item_colon' => __('父状态:', 'user-points-mall'), 'edit_item' => __('编辑状态', 'user-points-mall'), 'update_item' => __('更新状态', 'user-points-mall'), 'add_new_item' => __('添加新状态', 'user-points-mall'), 'new_item_name' => __('新状态名称', 'user-points-mall'), 'menu_name' => __('贡献状态', 'user-points-mall'), ); $args = array( 'hierarchical' => true, 'labels' => $labels, 'show_ui' => true, 'show_admin_column' => true, 'query_var' => true, 'rewrite' => array('slug' => 'contribution-status'), 'show_in_rest' => true, ); register_taxonomy('contribution_status', array('user_contribution'), $args); // 默认状态 $default_statuses = array( 'pending' => __('待审核', 'user-points-mall'), 'approved' => __('已通过', 'user-points-mall'), 'rejected' => __('已拒绝', 'user-points-mall'), 'published' => __('已发布', 'user-points-mall'), ); foreach ($default_statuses as $slug => $name) { if (!term_exists($name, 'contribution_status')) { wp_insert_term($name, 'contribution_status', array('slug' => $slug)); } } } add_action('init', 'upm_register_contribution_status'); 2.2 前端用户提交表单 创建用户提交贡献内容的前端表单: // 短代码生成用户提交表单 function upm_contribution_form_shortcode($atts) { // 只有登录用户才能提交 if (!is_user_logged_in()) { return '<div class="upm-alert upm-alert-warning">' . __('请先登录后再提交贡献内容。', 'user-points-mall') . ' <a href="' . wp_login_url(get_permalink()) . '">' . __('登录', 'user-points-mall') . '</a></div>'; } // 处理表单提交 $message = ''; $success = false; if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['upm_contribution_nonce'])) { if (wp_verify_nonce($_POST['upm_contribution_nonce'], 'upm_submit_contribution')) { $result = upm_process_contribution_submission($_POST); if ($result['success']) { $message = '<div class="upm-alert upm-alert-success">' . __('贡献提交成功!内容正在审核中。', 'user-points-mall') . '</div>'; $success = true; } else { $message = '<div class="upm-alert upm-alert-error">' . $result['message'] . '</div>'; } } else { $message = '<div class="upm-alert upm-alert-error">' . __('安全验证失败,请重试。', 'user-points-mall') . '</div>'; } } // 如果提交成功,显示成功消息 if ($success) { return $message; } // 输出表单 ob_start(); ?> <div class="upm-contribution-form-wrapper"> <?php echo $message; ?> <form id="upm-contribution-form" method="post" enctype="multipart/form-data"> <?php wp_nonce_field('upm_submit_contribution', 'upm_contribution_nonce'); ?> <div class="upm-form-group"> <label for="upm-contribution-title"><?php _e('标题', 'user-points-mall'); ?> *</label> <input type="text" id="upm-contribution-title" name="contribution_title" required class="upm-form-control" value="<?php echo isset($_POST['contribution_title']) ? esc_attr($_POST['contribution_title']) : ''; ?>"> </div> <div class="upm-form-group"> <label for="upm-contribution-content"><?php _e('内容', 'user-points-mall'); ?> *</label> <?php $content = isset($_POST['contribution_content']) ? wp_kses_post($_POST['contribution_content']) : ''; wp_editor($content, 'upm-contribution-content', array( 'textarea_name' => 'contribution_content', 'textarea_rows' => 10, 'media_buttons' => true, 'teeny' => false, 'quicktags' => true )); ?> </div> <div class="upm-form-group"> <label for="upm-contribution-excerpt"><?php _e('摘要', 'user-points-mall'); ?></label> <textarea id="upm-contribution-excerpt" name="contribution_excerpt" class="upm-form-control" rows="3"><?php echo isset($_POST['contribution_excerpt']) ? esc_textarea($_POST['contribution_excerpt']) : ''; ?></textarea> </div> <div class="upm-form-group"> <label for="upm-contribution-thumbnail"><?php _e('特色图片', 'user-points-mall'); ?></label> <input type="file" id="upm-contribution-thumbnail" name="contribution_thumbnail" accept="image/*" class="upm-form-control"> <p class="upm-help-text"><?php _e('支持JPG、PNG格式,最大2MB', 'user-points-mall'); ?></p> </div> <div class="upm-form-group"> <label for="upm-contribution-category"><?php _e('分类', 'user-points-mall'); ?></label> <select id="upm-contribution-category" name="contribution_category" class="upm-form-control"> <option value=""><?php _e('选择分类', 'user-points-mall'); ?></option> <?php $categories = get_categories(array('hide_empty' => false)); foreach ($categories as $category) { $selected = (isset($_POST['contribution_category']) && $_POST['contribution_category'] == $category->term_id) ? 'selected' : ''; echo '<option value="' . $category->term_id . '" ' . $selected . '>' . esc_html($category->name) . '</option>'; } ?> </select> </div> <div class="upm-form-group"> <label> <input type="checkbox" name="agree_terms" required> <?php _e('我同意遵守网站内容提交规则', 'user-points-mall'); ?> </label> </div> <div class="upm-form-group"> <input type="submit" name="submit_contribution" value="<?php _e('提交贡献', 'user-points-mall'); ?>" class="upm-btn upm-btn-primary"> </div> </form> </div> <style> .upm-contribution-form-wrapper { max-width: 800px; margin: 0 auto; padding: 20px; background: #f9f9f9; border-radius: 8px; } .upm-form-group { margin-bottom: 20px; } .upm-form-control { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; } .upm-btn { padding: 12px 24px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; } .upm-btn-primary { background: #0073aa; color: white; } .upm-alert { padding: 15px; margin-bottom: 20px; border-radius: 4px; } .upm-alert-success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; } .upm-alert-error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } .upm-alert-warning { background: #fff3cd; color: #856404; border: 1px solid #ffeaa7; } </style> <?php return ob_get_clean(); } add_shortcode('upm_contribution_form', 'upm_contribution_form_shortcode'); // 处理表单提交 function upm_process_contribution_submission($data) { $result = array('success' => false, 'message' => ''); // 验证数据 if (empty($data['contribution_title'])) { $result['message'] = __('标题不能为空', 'user-points-mall'); return $result; } if (empty($data['contribution_content'])) { $result['message'] = __('内容不能为空', 'user-points-mall'); return $result; } if (!isset($data['agree_terms'])) { $result['message'] = __('请同意内容提交规则', 'user-points-mall'); return $result; } // 准备文章数据 $post_data = array( 'post_title' => sanitize_text_field($data['contribution_title']), 'post_content' => wp_kses_post($data['contribution_content']), 'post_excerpt' => sanitize_textarea_field($data['contribution_excerpt']), 'post_type' => 'user_contribution', 'post_status' => 'pending', // 初始状态为待审核 'post_author' => get_current_user_id(), ); // 插入文章 $post_id = wp_insert_post($post_data); if (is_wp_error($post_id) || $post_id === 0) { $result['message'] = __('提交失败,请重试', 'user-points-mall'); return $result; } // 设置分类 if (!empty($data['contribution_category'])) { wp_set_post_categories($post_id, array(intval($data['contribution_category']))); } // 设置默认状态为"待审核" wp_set_object_terms($post_id, 'pending', 'contribution_status'); // 处理特色图片上传 thumbnail']['name'])) { $upload = upm_handle_image_upload($post_id, 'contribution_thumbnail'); if ($upload && !is_wp_error($upload)) { set_post_thumbnail($post_id, $upload); } } // 记录用户贡献行为并分配积分 upm_award_points_for_contribution(get_current_user_id(), $post_id); // 发送通知邮件给管理员 upm_send_contribution_notification($post_id); $result['success'] = true; return $result; } // 处理图片上传function upm_handle_image_upload($post_id, $field_name) { if (!function_exists('wp_handle_upload')) { require_once(ABSPATH . 'wp-admin/includes/file.php'); } $uploadedfile = $_FILES[$field_name]; $upload_overrides = array('test_form' => false); // 验证文件类型和大小 $file_type = wp_check_filetype($uploadedfile['name']); $allowed_types = array('jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png'); if (!in_array($file_type['type'], $allowed_types)) { return new WP_Error('invalid_type', __('只支持JPG和PNG格式', 'user-points-mall')); } if ($uploadedfile['size'] > 2 * 1024 * 1024) { // 2MB限制 return new WP_Error('file_size', __('文件大小不能超过2MB', 'user-points-mall')); } $movefile = wp_handle_upload($uploadedfile, $upload_overrides); if ($movefile && !isset($movefile['error'])) { $filename = $movefile['file']; $filetype = wp_check_filetype($filename, null); $attachment = array( 'post_mime_type' => $filetype['type'], 'post_title' => sanitize_file_name($uploadedfile['name']), 'post_content' => '', 'post_status' => 'inherit', 'post_parent' => $post_id ); $attach_id = wp_insert_attachment($attachment, $filename, $post_id); require_once(ABSPATH . 'wp-admin/includes/image.php'); $attach_data = wp_generate_attachment_metadata($attach_id, $filename); wp_update_attachment_metadata($attach_id, $attach_data); return $attach_id; } return false; } ### 2.3 用户贡献内容展示与管理 创建用户贡献内容展示页面和管理功能: // 短代码显示用户贡献列表function upm_user_contributions_shortcode($atts) { $atts = shortcode_atts(array( 'user_id' => get_current_user_id(), 'per_page' => 10, 'status' => 'all' ), $atts, 'upm_user_contributions'); if (!is_user_logged_in() && $atts['user_id'] == get_current_user_id()) { return '<div class="upm-alert upm-alert-warning">' . __('请先登录查看您的贡献', 'user-points-mall') . '</div>'; } $paged = get_query_var('paged') ? get_query_var('paged') : 1; $args = array( 'post_type' => 'user_contribution', 'author' => $atts['user_id'], 'posts_per_page' => $atts['per_page'], 'paged' => $paged, 'post_status' => 'any', ); if ($atts['status'] != 'all') { $args['tax_query'] = array( array( 'taxonomy' => 'contribution_status', 'field' => 'slug', 'terms' => $atts['status'] ) ); } $contributions = new WP_Query($args); ob_start(); ?> <div class="upm-user-contributions"> <h3><?php _e('我的贡献', 'user-points-mall'); ?></h3> <?php if ($contributions->have_posts()) : ?> <div class="upm-contributions-list"> <?php while ($contributions->have_posts()) : $contributions->the_post(); ?> <div class="upm-contribution-item"> <h4><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h4> <div class="upm-contribution-meta"> <span class="upm-date"><?php echo get_the_date(); ?></span> <span class="upm-status"> <?php $status_terms = get_the_terms(get_the_ID(), 'contribution_status'); if ($status_terms && !is_wp_error($status_terms)) { $status = reset($status_terms); echo '<span class="status-badge status-' . $status->slug . '">' . $status->name . '</span>'; } ?> </span> <span class="upm-points"> <?php $points = get_post_meta(get_the_ID(), '_contribution_points', true); if ($points) { echo sprintf(__('获得积分: %d', 'user-points-mall'), $points); } ?> </span> </div> <div class="upm-contribution-excerpt"> <?php the_excerpt(); ?> </div> </div> <?php endwhile; ?> </div> <!-- 分页 --> <div class="upm-pagination"> <?php echo paginate_links(array( 'total' => $contributions->max_num_pages, 'current' => $paged, 'prev_text' => __('« 上一页', 'user-points-mall'), 'next_text' => __('下一页 »', 'user-points-mall'), )); ?> </div> <?php wp_reset_postdata(); ?> <?php else : ?> <div class="upm-alert upm-alert-info"> <?php _e('暂无贡献内容', 'user-points-mall'); ?> </div> <?php endif; ?> </div> <style> .upm-user-contributions { margin: 20px 0; } .upm-contribution-item { border: 1px solid #e0e0e0; border-radius: 6px; padding: 15px; margin-bottom: 15px; background: white; } .upm-contribution-meta { display: flex; gap: 15px; margin: 10px 0; font-size: 14px; color: #666; } .status-badge { padding: 3px 8px; border-radius: 12px; font-size: 12px; font-weight: bold; } .status-pending { background: #fff3cd; color: #856404; } .status-approved { background: #d4edda; color: #155724; } .status-published { background: #cce5ff; color: #004085; } .status-rejected { background: #f8d7da; color: #721c24; } .upm-pagination { margin-top: 20px; text-align: center; } .upm-pagination a, .upm-pagination span { display: inline-block; padding: 8px 12px; margin: 0 2px; border: 1px solid #ddd; text-decoration: none; } .upm-pagination .current { background: #0073aa; color: white; border-color: #0073aa; } </style> <?php return ob_get_clean(); }add_shortcode('upm_user_contributions', 'upm_user_contributions_shortcode'); ## 第三部分:积分管理系统开发 ### 3.1 积分数据库设计与核心功能 创建积分管理系统的数据库表和核心功能: // 创建积分相关数据库表function upm_create_points_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'upm_user_points'; $log_table_name = $wpdb->prefix . 'upm_points_log'; // 用户积分表 $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, total_points int(11) NOT NULL DEFAULT 0, available_points int(11) NOT NULL DEFAULT 0, frozen_points int(11) NOT NULL DEFAULT 0, last_updated datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY user_id (user_id), KEY total_points (total_points), KEY available_points (available_points) ) $charset_collate;"; // 积分日志表 $sql .= "CREATE TABLE IF NOT EXISTS $log_table_name ( log_id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, points_change int(11) NOT NULL, new_balance int(11) NOT NULL, action_type varchar(50) NOT NULL, action_detail varchar(255) DEFAULT NULL, related_id bigint(20) DEFAULT NULL, related_type varchar(50) DEFAULT NULL, ip_address varchar(45) DEFAULT NULL, user_agent text, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (log_id), KEY user_id (user_id), KEY action_type (action_type), KEY created_at (created_at), KEY related_id (related_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); }register_activation_hook(__FILE__, 'upm_create_points_tables'); // 积分管理类class UPM_Points_Manager { private static $instance = null; public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } // 为用户添加积分 public function add_points($user_id, $points, $action_type, $action_detail = '', $related_id = null, $related_type = null) { if ($points <= 0) { return false; } global $wpdb; $table_name = $wpdb->prefix . 'upm_user_points'; // 获取或创建用户积分记录 $user_points = $this->get_user_points($user_id); if (!$user_points) { $wpdb->insert($table_name, array( 'user_id' => $user_id, 'total_points' => $points, 'available_points' => $points, 'frozen_points' => 0 )); } else { $wpdb->update($table_name, array( 'total_points' => $user_points->total_points + $points, 'available_points' => $user_points->available_points + $points ), array('user_id' => $user_id) ); } // 记录积分日志 $this->log_points_change($user_id, $points, $action_type, $action_detail, $related_id, $related_type); // 更新用户元数据缓存 delete_user_meta($user_id, '_upm_points_data'); return true; } // 扣除用户积分 public function deduct_points($user_id, $points, $action_type, $action_detail = '', $related_id = null, $related_type = null) { if ($points <= 0) { return false; } global $wpdb; $table_name = $wpdb->prefix . 'upm_user_points'; $user_points = $this->get_user_points($user_id); if (!$user_points || $user_points->available_points < $points) { return false; // 积分不足 } $wpdb->update($table_name, array( 'total_points' => $user_points->total_points - $points, 'available_points' => $user_points->available_points - $points ), array('user_id' => $user_id) ); // 记录积分日志(负值表示扣除) $this->log_points_change($user_id, -$points, $action_type, $action_detail, $related_id, $related_type); // 更新用户元数据缓存 delete_user_meta($user_id, '_upm_points_data'); return true; } // 冻结积分 public function freeze_points($user_id, $points, $action_type, $action_detail = '') { global $wpdb; $table_name = $wpdb->prefix . 'upm_user_points'; $user_points = $this->get_user_points($user_id); if (!$user_points || $user_points->available_points < $points) { return false; } $wpdb->update($table_name, array( 'available_points' => $user_points->available_points - $points, 'frozen_points' => $user_points->frozen_points + $points ), array('user_id' => $user_id) ); $this->log_points_change($user_id, -$points, $action_type . '_freeze', $action_detail); delete_user_meta($user_id, '_upm_points_data'); return true; } // 解冻积分 public function unfreeze_points($user_id, $points, $action_type, $action_detail = '') { global $wpdb; $table_name = $wpdb->prefix . 'upm_user_points'; $user_points = $this->get_user_points($user_id); if (!$user_points || $user_points->frozen_points < $points) { return false; } $wpdb->update($table_name, array( 'available_points' => $user_points->available_points + $points, 'frozen_points' => $user_points->frozen_points - $points ), array('user_id' => $user_id) ); $this->log_points_change($user_id, $points, $action_type . '_unfreeze', $action_detail); delete_user_meta($user_id, '_upm_points_data'); return true; } // 获取用户积分信息 public function get_user_points($user_id) { global $wpdb; $table_name = $wpdb->prefix . 'upm_user_points'; // 尝试从缓存获取 $cached = get_user_meta($user_id, '_upm_points_data', true); if ($cached && is_object($cached)) { return $cached; } $result = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE user_id = %d", $user_id )); if ($result) { // 缓存结果 update_user_meta($user_id, '_upm_points_data', $result); } return $result; } // 记录积分变化日志 private function log_points_change($user_id, $points_change, $action_type, $action_detail, $related_id = null, $related_type = null) { global $wpdb; $table_name = $wpdb->prefix . 'upm_points_log'; $user_points = $this->get_user_points($user_id); $new_balance = $user_points ? $user_points->available_points : 0; $data = array( 'user_id' => $user_id, 'points_change' => $points_change, 'new_balance' => $new_balance, 'action_type' => $action_type, 'action_detail' => $action_detail, 'related_id' => $related_id, 'related_type' => $related_type, 'ip_address' => $this->get_client_ip(), 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '' ); $wpdb->insert($table_name, $data); // 触发积分变化钩子 do_action('upm_points_changed', $user_id, $points_change, $action_type, $data); } // 获取客户端IP private function get_client_ip() { $ip_keys = array('HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR'); foreach ($ip_keys as $key) { if (array_key_exists($key, $_SERVER) === true) { foreach (explode(',', $_SERVER[$key]) as $ip) { $ip = trim($ip); if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) { return $ip; } } } } return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0'; } // 获取积分排行榜 public function get_points_leaderboard($limit = 10, $offset = 0) { global $wpdb; $table_name = $wpdb->prefix

发表评论

实战教学,为你的网站添加在线团队投票与决策辅助工具

实战教学:为你的网站添加在线团队投票与决策辅助工具 引言:为什么网站需要团队协作工具? 在当今数字化工作环境中,团队协作效率直接影响项目成败。无论是小型创业团队、远程工作小组,还是企业内部部门,都需要高效的工具来收集意见、做出决策。然而,许多团队仍然依赖分散的沟通渠道——微信群讨论、邮件投票、Excel表格统计——这种方式效率低下且容易造成信息丢失。 通过为你的WordPress网站添加在线投票与决策辅助工具,你可以创建一个集中的协作平台,让团队成员能够实时参与决策过程,记录讨论结果,并追踪决策历史。这不仅提高了团队效率,还创造了透明、可追溯的决策文化。 本教程将带你一步步实现这一功能,无需购买昂贵的第三方服务,通过代码二次开发即可拥有完全可控的团队协作工具。 第一部分:准备工作与环境搭建 1.1 开发环境要求 在开始之前,请确保你拥有以下环境: WordPress 5.0及以上版本 PHP 7.2及以上版本(推荐7.4+) MySQL 5.6及以上版本 一个代码编辑器(如VS Code、Sublime Text等) 基础的HTML、CSS、JavaScript和PHP知识 1.2 创建开发用的子主题 为了避免主题更新导致代码丢失,我们首先创建一个子主题: 在WordPress的wp-content/themes/目录下创建新文件夹team-vote-theme 在该文件夹中创建style.css文件,添加以下内容: /* Theme Name: Team Vote Theme Template: 你的父主题名称 Version: 1.0 Description: 用于团队投票功能的子主题 */ /* 导入父主题样式 */ @import url("../父主题名称/style.css"); 创建functions.php文件,添加以下代码: <?php // 子主题初始化 function team_vote_theme_setup() { // 加载父主题样式 add_action('wp_enqueue_scripts', 'enqueue_parent_styles'); } add_action('after_setup_theme', 'team_vote_theme_setup'); function enqueue_parent_styles() { wp_enqueue_style('parent-style', get_template_directory_uri().'/style.css'); } // 在这里添加我们的自定义功能 ?> 在WordPress后台启用这个子主题 1.3 创建插件还是主题功能? 对于这种功能,我们有三种实现方式: 作为主题的一部分(适合特定主题) 创建独立插件(适合多站点或主题无关场景) 使用功能插件(Functions Plugin)模式 本教程将采用功能插件模式,这样即使更换主题,功能也能保留。在wp-content/plugins/目录下创建team-vote-tool文件夹,并创建主文件team-vote-tool.php: <?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('TEAM_VOTE_VERSION', '1.0.0'); define('TEAM_VOTE_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('TEAM_VOTE_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 require_once TEAM_VOTE_PLUGIN_DIR . 'includes/init.php'; ?> 第二部分:数据库设计与数据模型 2.1 创建自定义数据库表 团队投票系统需要存储投票活动、选项、用户投票记录等信息。我们在插件激活时创建必要的数据库表: 在插件目录下创建includes/activate.php: <?php function team_vote_activate_plugin() { // 检查必要的WordPress函数 if (!function_exists('dbDelta')) { require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); } global $wpdb; $charset_collate = $wpdb->get_charset_collate(); // 投票活动表 $table_name = $wpdb->prefix . 'team_votes'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, title varchar(255) NOT NULL, description text, creator_id bigint(20) NOT NULL, status varchar(20) DEFAULT 'active', vote_type varchar(50) DEFAULT 'single', max_choices mediumint(9) DEFAULT 1, start_date datetime DEFAULT NULL, end_date datetime DEFAULT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) $charset_collate;"; dbDelta($sql); // 投票选项表 $table_name = $wpdb->prefix . 'team_vote_options'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, vote_id mediumint(9) NOT NULL, option_text varchar(500) NOT NULL, description text, image_url varchar(500), created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY vote_id (vote_id) ) $charset_collate;"; dbDelta($sql); // 投票记录表 $table_name = $wpdb->prefix . 'team_vote_records'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, vote_id mediumint(9) NOT NULL, option_id mediumint(9) NOT NULL, user_id bigint(20) NOT NULL, comment text, voted_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY user_vote_unique (vote_id, user_id, option_id), KEY vote_id (vote_id), KEY user_id (user_id) ) $charset_collate;"; dbDelta($sql); // 决策讨论表 $table_name = $wpdb->prefix . 'team_decisions'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, title varchar(255) NOT NULL, description text, creator_id bigint(20) NOT NULL, status varchar(20) DEFAULT 'discussion', final_decision text, deadline datetime DEFAULT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) $charset_collate;"; dbDelta($sql); // 决策评论表 $table_name = $wpdb->prefix . 'team_decision_comments'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, decision_id mediumint(9) NOT NULL, user_id bigint(20) NOT NULL, comment text NOT NULL, sentiment varchar(20) DEFAULT 'neutral', created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY decision_id (decision_id), KEY user_id (user_id) ) $charset_collate;"; dbDelta($sql); // 存储插件版本,便于后续更新 add_option('team_vote_db_version', TEAM_VOTE_VERSION); } register_activation_hook(__FILE__, 'team_vote_activate_plugin'); ?> 2.2 创建数据模型类 为了更方便地操作数据,我们创建模型类。在includes/models/目录下创建Vote.php: <?php class TeamVoteModel { private $wpdb; private $table_votes; private $table_options; private $table_records; public function __construct() { global $wpdb; $this->wpdb = $wpdb; $this->table_votes = $wpdb->prefix . 'team_votes'; $this->table_options = $wpdb->prefix . 'team_vote_options'; $this->table_records = $wpdb->prefix . 'team_vote_records'; } // 创建新投票 public function create_vote($data) { $defaults = array( 'status' => 'active', 'vote_type' => 'single', 'max_choices' => 1, 'created_at' => current_time('mysql') ); $data = wp_parse_args($data, $defaults); $result = $this->wpdb->insert( $this->table_votes, $data ); if ($result) { return $this->wpdb->insert_id; } return false; } // 获取投票详情 public function get_vote($vote_id) { $query = $this->wpdb->prepare( "SELECT * FROM $this->table_votes WHERE id = %d", $vote_id ); return $this->wpdb->get_row($query); } // 获取用户的所有投票 public function get_user_votes($user_id, $status = null) { $where = "WHERE creator_id = %d"; $params = array($user_id); if ($status) { $where .= " AND status = %s"; $params[] = $status; } $query = $this->wpdb->prepare( "SELECT * FROM $this->table_votes $where ORDER BY created_at DESC", $params ); return $this->wpdb->get_results($query); } // 添加投票选项 public function add_option($vote_id, $option_text, $description = '', $image_url = '') { return $this->wpdb->insert( $this->table_options, array( 'vote_id' => $vote_id, 'option_text' => $option_text, 'description' => $description, 'image_url' => $image_url ) ); } // 获取投票的所有选项 public function get_options($vote_id) { $query = $this->wpdb->prepare( "SELECT * FROM $this->table_options WHERE vote_id = %d ORDER BY id ASC", $vote_id ); return $this->wpdb->get_results($query); } // 记录用户投票 public function record_vote($vote_id, $option_id, $user_id, $comment = '') { // 检查是否已经投过票(对于单选投票) $vote = $this->get_vote($vote_id); if ($vote->vote_type === 'single') { $existing = $this->wpdb->get_var($this->wpdb->prepare( "SELECT COUNT(*) FROM $this->table_records WHERE vote_id = %d AND user_id = %d", $vote_id, $user_id )); if ($existing > 0) { return new WP_Error('already_voted', '您已经投过票了'); } } // 检查多选投票是否超过最大选择数 if ($vote->vote_type === 'multiple') { $current_choices = $this->wpdb->get_var($this->wpdb->prepare( "SELECT COUNT(*) FROM $this->table_records WHERE vote_id = %d AND user_id = %d", $vote_id, $user_id )); if ($current_choices >= $vote->max_choices) { return new WP_Error('max_choices', '已达到最大选择数量'); } } return $this->wpdb->insert( $this->table_records, array( 'vote_id' => $vote_id, 'option_id' => $option_id, 'user_id' => $user_id, 'comment' => $comment ) ); } // 获取投票结果 public function get_results($vote_id) { $query = $this->wpdb->prepare( "SELECT o.id, o.option_text, o.description, o.image_url, COUNT(r.id) as vote_count FROM $this->table_options o LEFT JOIN $this->table_records r ON o.id = r.option_id WHERE o.vote_id = %d GROUP BY o.id ORDER BY vote_count DESC", $vote_id ); return $this->wpdb->get_results($query); } // 检查用户是否已投票 public function has_user_voted($vote_id, $user_id) { $count = $this->wpdb->get_var($this->wpdb->prepare( "SELECT COUNT(*) FROM $this->table_records WHERE vote_id = %d AND user_id = %d", $vote_id, $user_id )); return $count > 0; } // 获取用户的投票选择 public function get_user_vote($vote_id, $user_id) { $query = $this->wpdb->prepare( "SELECT r.*, o.option_text FROM $this->table_records r JOIN $this->table_options o ON r.option_id = o.id WHERE r.vote_id = %d AND r.user_id = %d", $vote_id, $user_id ); return $this->wpdb->get_results($query); } } ?> 同样地,创建决策模型类Decision.php,由于篇幅限制,这里不展开详细代码。 第三部分:前端界面设计与实现 3.1 创建投票创建表单 在templates/目录下创建create-vote.php: <?php /** * 创建投票表单模板 */ if (!defined('ABSPATH')) { exit; } // 检查用户是否登录 if (!is_user_logged_in()) { echo '<div class="team-vote-message">请先登录以创建投票</div>'; return; } // 处理表单提交 if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_vote'])) { // 这里添加表单处理逻辑,将在后面实现 } // 获取当前用户ID $current_user_id = get_current_user_id(); ?> <div class="team-vote-container"> <h2>创建新投票</h2> <form id="create-vote-form" method="POST" action=""> <?php wp_nonce_field('create_vote_action', 'create_vote_nonce'); ?> <div class="form-group"> <label for="vote_title">投票标题 *</label> <input type="text" id="vote_title" name="vote_title" required placeholder="例如:选择团队下次团建地点"> </div> <div class="form-group"> <label for="vote_description">投票描述</label> <textarea id="vote_description" name="vote_description" rows="3" placeholder="详细描述投票的背景和目的"></textarea> </div> <div class="form-row"> <div class="form-group"> <label for="vote_type">投票类型</label> <select id="vote_type" name="vote_type"> <option value="single">单选</option> <option value="multiple">多选</option> </select> </div> <div class="form-group" id="max-choices-container" style="display:none;"> <label for="max_choices">最多可选数量</label> <input type="number" id="max_choices" name="max_choices" min="2" max="10" value="3"> </div> </div> <div class="form-row"> <div class="form-group"> <label for="start_date">开始时间</label> <input type="datetime-local" id="start_date" name="start_date"> </div> <div class="form-group"> <label for="end_date">结束时间</label> <input type="datetime-local" id="end_date" name="end_date"> </div> </div> <div class="form-group"> <label>投票选项 *</label> <div id="vote-options-container"> <div class="option-item"> <input type="text" name="options[]" placeholder="选项内容" required> <textarea name="option_descriptions[]" placeholder="选项描述(可选)"></textarea> <input type="text" name="option_images[]" placeholder="图片URL(可选)"> <button type="button" class="remove-option">删除</button> </div> <div class="option-item"> <input type="text" name="options[]" placeholder="选项内容" required> <textarea name="option_descriptions[]" placeholder="选项描述(可选)"></textarea> <input type="text" name="option_images[]" placeholder="图片URL(可选)"> <button type="button" class="remove-option">删除</button> </div> </div> <button type="button" id="add-option" class="button-secondary">添加选项</button> </div> <div class="form-submit"> <input type="submit" name="create_vote" value="创建投票" class="button-primary"> </div> </form> </div> <script> jQuery(document).ready(function($) { // 显示/隐藏多选数量设置 $('#vote_type').change(function() { if ($(this).val() === 'multiple') { $('#max-choices-container').show(); } else { $('#max-choices-container').hide(); } }); // 添加选项 $('#add-option').click(function() { var optionHtml = ` <div class="option-item"> 选项内容" required> <textarea name="option_descriptions[]" placeholder="选项描述(可选)"></textarea> <input type="text" name="option_images[]" placeholder="图片URL(可选)"> <button type="button" class="remove-option">删除</button> </div>`; $('#vote-options-container').append(optionHtml); }); // 删除选项 $(document).on('click', '.remove-option', function() { if ($('.option-item').length > 2) { $(this).closest('.option-item').remove(); } else { alert('投票至少需要两个选项'); } }); });</script> ### 3.2 创建投票展示与参与界面 在`templates/`目录下创建`vote-display.php`: <?php/** 投票展示与参与模板 */ if (!defined('ABSPATH')) { exit; } // 获取投票ID$vote_id = isset($_GET['vote_id']) ? intval($_GET['vote_id']) : 0;if (!$vote_id) { echo '<div class="team-vote-message">未指定投票</div>'; return; } // 获取投票信息$vote_model = new TeamVoteModel();$vote = $vote_model->get_vote($vote_id);if (!$vote) { echo '<div class="team-vote-message">投票不存在</div>'; return; } // 获取投票选项$options = $vote_model->get_options($vote_id);if (empty($options)) { echo '<div class="team-vote-message">投票选项未设置</div>'; return; } // 检查投票状态$current_time = current_time('mysql');$is_active = true; if ($vote->start_date && $vote->start_date > $current_time) { $is_active = false; $status_message = '投票尚未开始'; } elseif ($vote->end_date && $vote->end_date < $current_time) { $is_active = false; $status_message = '投票已结束'; } elseif ($vote->status !== 'active') { $is_active = false; $status_message = '投票已关闭'; } // 获取当前用户信息$current_user_id = get_current_user_id();$has_voted = $vote_model->has_user_voted($vote_id, $current_user_id);$user_votes = $has_voted ? $vote_model->get_user_vote($vote_id, $current_user_id) : array(); // 获取投票结果$results = $vote_model->get_results($vote_id);$total_votes = 0;foreach ($results as $result) { $total_votes += $result->vote_count; } // 处理投票提交if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['submit_vote']) && $is_active && !$has_voted) { // 这里添加投票处理逻辑 }?> <div class="team-vote-container"> <div class="vote-header"> <h2><?php echo esc_html($vote->title); ?></h2> <div class="vote-meta"> <span class="vote-creator">创建者: <?php echo get_userdata($vote->creator_id)->display_name; ?></span> <span class="vote-date">创建时间: <?php echo date('Y-m-d H:i', strtotime($vote->created_at)); ?></span> <?php if ($vote->end_date): ?> <span class="vote-deadline">截止时间: <?php echo date('Y-m-d H:i', strtotime($vote->end_date)); ?></span> <?php endif; ?> </div> <?php if ($vote->description): ?> <div class="vote-description"> <?php echo wpautop(esc_html($vote->description)); ?> </div> <?php endif; ?> <div class="vote-status"> <?php if ($is_active): ?> <span class="status-active">● 进行中</span> <?php else: ?> <span class="status-inactive">● <?php echo $status_message; ?></span> <?php endif; ?> <span class="total-votes">总投票数: <?php echo $total_votes; ?></span> </div> </div> <?php if ($is_active && !$has_voted): ?> <form id="vote-form" method="POST" action=""> <?php wp_nonce_field('vote_action_' . $vote_id, 'vote_nonce'); ?> <input type="hidden" name="vote_id" value="<?php echo $vote_id; ?>"> <div class="vote-options"> <h3>请选择<?php echo $vote->vote_type === 'multiple' ? '(可多选)' : '(单选)'; ?></h3> <?php foreach ($options as $index => $option): ?> <div class="vote-option"> <label class="option-label"> <?php if ($vote->vote_type === 'multiple'): ?> <input type="checkbox" name="selected_options[]" value="<?php echo $option->id; ?>" class="option-checkbox"> <?php else: ?> <input type="radio" name="selected_options" value="<?php echo $option->id; ?>" class="option-radio" required> <?php endif; ?> <div class="option-content"> <div class="option-text"><?php echo esc_html($option->option_text); ?></div> <?php if ($option->description): ?> <div class="option-description"> <?php echo esc_html($option->description); ?> </div> <?php endif; ?> <?php if ($option->image_url): ?> <div class="option-image"> <img src="<?php echo esc_url($option->image_url); ?>" alt="<?php echo esc_attr($option->option_text); ?>"> </div> <?php endif; ?> </div> </label> </div> <?php endforeach; ?> </div> <div class="vote-comment"> <label for="vote_comment">投票备注(可选)</label> <textarea id="vote_comment" name="vote_comment" placeholder="请说明您选择此选项的理由..."></textarea> </div> <div class="vote-submit"> <button type="submit" name="submit_vote" class="button-primary"> 提交投票 </button> </div> </form> <?php elseif ($has_voted): ?> <div class="already-voted"> <h3>您已参与本次投票</h3> <p>您选择了以下选项:</p> <ul class="user-selections"> <?php foreach ($user_votes as $user_vote): ?> <li><?php echo esc_html($user_vote->option_text); ?></li> <?php endforeach; ?> </ul> <?php if (!empty($user_votes[0]->comment)): ?> <div class="user-comment"> <strong>您的备注:</strong> <p><?php echo esc_html($user_votes[0]->comment); ?></p> </div> <?php endif; ?> </div> <?php endif; ?> <!-- 投票结果展示 --> <div class="vote-results"> <h3>投票结果</h3> <?php if ($total_votes > 0): ?> <div class="results-summary"> <p>总参与人数: <strong><?php echo $total_votes; ?></strong></p> </div> <div class="results-chart"> <?php foreach ($results as $result): $percentage = $total_votes > 0 ? round(($result->vote_count / $total_votes) * 100, 1) : 0; ?> <div class="result-item"> <div class="result-header"> <span class="option-text"><?php echo esc_html($result->option_text); ?></span> <span class="vote-count"><?php echo $result->vote_count; ?> 票 (<?php echo $percentage; ?>%)</span> </div> <div class="result-bar-container"> <div class="result-bar" style="width: <?php echo $percentage; ?>%;"></div> </div> <?php if ($result->description): ?> <div class="result-description"> <?php echo esc_html($result->description); ?> </div> <?php endif; ?> </div> <?php endforeach; ?> </div> <?php else: ?> <p class="no-results">暂无投票结果</p> <?php endif; ?> </div> </div> <?php if ($is_active && !$has_voted): ?><script>jQuery(document).ready(function($) { // 多选投票的最大选择限制 <?php if ($vote->vote_type === 'multiple' && $vote->max_choices > 0): ?> var maxChoices = <?php echo $vote->max_choices; ?>; $('.option-checkbox').change(function() { var checkedCount = $('.option-checkbox:checked').length; if (checkedCount > maxChoices) { $(this).prop('checked', false); alert('最多只能选择 ' + maxChoices + ' 个选项'); } }); <?php endif; ?> // 表单提交验证 $('#vote-form').submit(function(e) { var selectedCount = $('.option-checkbox:checked').length + ($('.option-radio:checked').length ? 1 : 0); if (selectedCount === 0) { e.preventDefault(); alert('请至少选择一个选项'); return false; } <?php if ($vote->vote_type === 'multiple'): ?> if (selectedCount > maxChoices) { e.preventDefault(); alert('最多只能选择 ' + maxChoices + ' 个选项'); return false; } <?php endif; ?> }); });</script><?php endif; ?> ### 3.3 创建决策讨论板界面 在`templates/`目录下创建`decision-board.php`: <?php/** 决策讨论板模板 */ if (!defined('ABSPATH')) { exit; } // 获取决策ID$decision_id = isset($_GET['decision_id']) ? intval($_GET['decision_id']) : 0;if (!$decision_id) { echo '<div class="team-vote-message">未指定决策讨论</div>'; return; } // 获取决策信息$decision_model = new TeamDecisionModel();$decision = $decision_model->get_decision($decision_id);if (!$decision) { echo '<div class="team-vote-message">决策讨论不存在</div>'; return; } // 获取讨论评论$comments = $decision_model->get_comments($decision_id); // 获取当前用户信息$current_user_id = get_current_user_id();$current_user = wp_get_current_user(); // 处理评论提交if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['submit_comment'])) { // 这里添加评论处理逻辑 }?> <div class="decision-container"> <div class="decision-header"> <h2><?php echo esc_html($decision->title); ?></h2> <div class="decision-meta"> <span class="decision-creator">发起人: <?php echo get_userdata($decision->creator_id)->display_name; ?></span> <span class="decision-date">创建时间: <?php echo date('Y-m-d H:i', strtotime($decision->created_at)); ?></span> <?php if ($decision->deadline): ?> <span class="decision-deadline">截止时间: <?php echo date('Y-m-d H:i', strtotime($decision->deadline)); ?></span> <?php endif; ?> <span class="decision-status status-<?php echo $decision->status; ?>"> <?php $status_labels = array( 'discussion' => '讨论中', 'voting' => '投票中', 'decided' => '已决定', 'closed' => '已关闭' ); echo $status_labels[$decision->status] ?? $decision->status; ?> </span> </div> <?php if ($decision->description): ?> <div class="decision-description"> <h3>决策背景</h3> <?php echo wpautop(esc_html($decision->description)); ?> </div> <?php endif; ?> <?php if ($decision->final_decision && $decision->status === 'decided'): ?> <div class="final-decision"> <h3>最终决定</h3> <div class="decision-content"> <?php echo wpautop(esc_html($decision->final_decision)); ?> </div> <div class="decision-meta"> 决定时间: <?php echo date('Y-m-d H:i', strtotime($decision->updated_at)); ?> </div> </div> <?php endif; ?> </div> <!-- 讨论区 --> <div class="discussion-section"> <h3>讨论区 (<?php echo count($comments); ?> 条评论)</h3> <?php if ($decision->status === 'discussion' || $decision->status === 'voting'): ?> <div class="comment-form"> <form method="POST" action=""> <?php wp_nonce_field('comment_action_' . $decision_id, 'comment_nonce'); ?> <div class="form-group"> <label for="comment_content">发表您的观点</label> <textarea id="comment_content" name="comment_content" rows="4" required placeholder="请详细阐述您的观点和建议..."></textarea> </div> <div class="form-group"> <label for="comment_sentiment">观点倾向</label> <select id="comment_sentiment" name="comment_sentiment"> <option value="support">支持</option> <option value="neutral" selected>中立</option> <option value="oppose">反对</option> <option value="question">疑问</option> <option value="suggestion">建议</option> </select> </div> <div class="form-submit"> <button type="submit" name="submit_comment" class="button-primary"> 发表评论 </button> </div> </form> </div> <?php endif; ?> <div class="comments-list"> <?php if (empty($comments)): ?> <p class="no-comments">暂无讨论,快来发表第一个观点吧!</p> <?php else: ?> <?php foreach ($comments as $comment): $comment_user = get_userdata($comment->user_id); $sentiment_classes = array( 'support' => 'sentiment-support', 'oppose' => 'sentiment-oppose', 'question' => 'sentiment-question', 'suggestion' => 'sentiment-suggestion', 'neutral' => 'sentiment-neutral' ); $sentiment_labels = array( 'support' => '支持', 'oppose' => '反对', 'question' => '疑问', 'suggestion' => '建议', 'neutral' => '中立' ); ?> <div class="comment-item <?php echo $sentiment_classes[$comment->sentiment] ?? ''; ?>"> <div class="comment-header"> <div class="comment-author"> <span class="author-avatar"> <?php echo get_avatar($comment->user_id, 40); ?> </span> <span class="author-name"><?php echo $comment_user->display_name; ?></span> <span class="comment-sentiment"> <?php echo $sentiment_labels[$comment->sentiment] ?? $comment->sentiment; ?> </span> </div> <div class="comment-date"> <?php echo date('Y-m-d H:i', strtotime($comment->created_at)); ?> </div> </div> <div class="comment-content"> <?php echo wpautop(esc_html($comment->comment)); ?> </div> </div> <?php endforeach; ?> <?php endif; ?> </div> </div> <!-- 相关投票(如果存在) --> <?php // 获取与决策相关的投票 $related_votes = $decision_model->get_related_votes($decision_id); if (!empty($related_votes)): ?> <div class="related-votes"> <h3>相关投票</h3> <div class="votes-list"> <?php foreach ($related_votes as $vote): ?> <div class="vote-summary"> <h4><?php echo esc_html($vote->title); ?></h4> <p><?php echo esc_html(wp_trim_words($vote->description, 20)); ?></p> <a href="?page=team_vote&vote_id=<?php echo $vote->id; ?>" class="button-small"> 查看投票 </a> </div> <?php endforeach; ?> </div> </div> <?php endif; ?> </div> <style>.sentiment-support { border-left: 4px solid #4CAF50; }.sentiment-oppose { border-left: 4px solid #F44336; }.sentiment-question { border-left: 4px solid #FF9800; }.sentiment-suggestion { border-left: 4px solid #2196F3; }.sentiment-neutral { border-left: 4px solid #9E9E9E; }</style> ## 第四部分:后端逻辑与AJAX处理 ### 4.1 表单处理与数据验证

发表评论

手把手教程,在WordPress中集成网站第三方服务API统一监控面板

手把手教程:在WordPress中集成网站第三方服务API统一监控面板 引言:为什么需要API统一监控面板? 在当今的互联网环境中,网站通常依赖于多个第三方服务API来增强功能、提升用户体验或实现特定业务需求。从支付网关、社交媒体集成、邮件服务到数据分析工具,这些API服务构成了现代网站的核心支撑体系。然而,随着集成服务的增多,管理这些API的状态、监控其性能和可用性变得越来越复杂。 想象一下这样的场景:您的电商网站集成了支付宝、微信支付、物流查询、库存管理、邮件通知等十几种API服务。当某个服务出现故障时,您可能需要逐一检查每个服务的状态,这不仅耗时耗力,而且可能延误问题的解决时间,直接影响用户体验和业务收入。 本教程将手把手教您如何在WordPress中创建一个统一的第三方服务API监控面板,通过代码二次开发实现常用互联网小工具功能,让您能够在一个界面中实时监控所有集成的API服务状态,快速定位问题,提高网站运维效率。 第一部分:准备工作与环境配置 1.1 确定监控需求与目标API 在开始开发之前,首先需要明确您要监控哪些第三方服务API。常见的监控目标包括: 支付网关:支付宝、微信支付、PayPal等 社交媒体:微信、微博、Facebook、Twitter等API 邮件服务:SMTP服务、SendGrid、MailChimp等 云存储:阿里云OSS、腾讯云COS、AWS S3等 地图服务:百度地图、高德地图、Google Maps等 短信服务:阿里云短信、腾讯云短信等 数据分析:Google Analytics、百度统计等 列出所有需要监控的API后,记录每个API的以下信息: API端点URL 认证方式(API密钥、OAuth等) 健康检查方法(专用健康检查端点或模拟实际请求) 预期响应时间和格式 1.2 创建WordPress插件框架 我们将创建一个独立的WordPress插件来实现API监控功能,这样可以确保代码的独立性和可维护性。 在WordPress的wp-content/plugins/目录下创建新文件夹api-monitor-dashboard 在该文件夹中创建主插件文件api-monitor-dashboard.php,并添加以下基础代码: <?php /** * Plugin Name: API统一监控面板 * Plugin URI: https://yourwebsite.com/ * Description: 在WordPress中集成第三方服务API统一监控面板 * Version: 1.0.0 * Author: 您的名字 * License: GPL v2 or later * Text Domain: api-monitor-dashboard */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('AMD_VERSION', '1.0.0'); define('AMD_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('AMD_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 require_once AMD_PLUGIN_DIR . 'includes/class-api-monitor-core.php'; function amd_init() { $api_monitor = new API_Monitor_Core(); $api_monitor->init(); } add_action('plugins_loaded', 'amd_init'); 1.3 创建数据库表结构 为了存储API监控的历史数据和配置,我们需要创建数据库表。在插件初始化时检查并创建必要的表结构。 在includes/class-api-monitor-core.php中添加数据库创建方法: class API_Monitor_Core { public function init() { // 注册激活钩子 register_activation_hook(__FILE__, array($this, 'activate_plugin')); // 注册管理菜单 add_action('admin_menu', array($this, 'add_admin_menu')); // 加载必要资源 add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets')); } public function activate_plugin() { $this->create_database_tables(); $this->insert_default_data(); } private function create_database_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'api_monitor_services'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, service_name varchar(100) NOT NULL, api_endpoint varchar(500) NOT NULL, api_type varchar(50) NOT NULL, auth_method varchar(50) DEFAULT 'api_key', api_key varchar(500) DEFAULT '', check_interval int DEFAULT 300, last_check datetime DEFAULT '0000-00-00 00:00:00', last_status varchar(20) DEFAULT 'unknown', last_response_time float DEFAULT 0, uptime_percentage float DEFAULT 100, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, is_active tinyint(1) DEFAULT 1, PRIMARY KEY (id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 创建监控日志表 $log_table_name = $wpdb->prefix . 'api_monitor_logs'; $sql_log = "CREATE TABLE IF NOT EXISTS $log_table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, service_id mediumint(9) NOT NULL, check_time datetime DEFAULT CURRENT_TIMESTAMP, status varchar(20) NOT NULL, response_time float NOT NULL, response_code int DEFAULT 0, error_message text, PRIMARY KEY (id), KEY service_id (service_id), KEY check_time (check_time) ) $charset_collate;"; dbDelta($sql_log); } private function insert_default_data() { // 可以在这里插入一些默认的API服务配置 } } 第二部分:构建API监控核心功能 2.1 设计API服务检查器 API监控的核心是能够定期检查各个服务的状态。我们将创建一个服务检查器类,负责执行API健康检查。 创建includes/class-api-checker.php: class API_Checker { private $wpdb; private $services_table; private $logs_table; public function __construct() { global $wpdb; $this->wpdb = $wpdb; $this->services_table = $wpdb->prefix . 'api_monitor_services'; $this->logs_table = $wpdb->prefix . 'api_monitor_logs'; } /** * 检查所有活跃的API服务 */ public function check_all_services() { $services = $this->get_active_services(); foreach ($services as $service) { $this->check_single_service($service); } return count($services); } /** * 获取所有活跃的服务 */ private function get_active_services() { return $this->wpdb->get_results( "SELECT * FROM {$this->services_table} WHERE is_active = 1" ); } /** * 检查单个API服务 */ private function check_single_service($service) { $start_time = microtime(true); $status = 'unknown'; $response_code = 0; $error_message = ''; try { // 根据API类型选择不同的检查方法 switch ($service->api_type) { case 'http_get': $result = $this->check_http_get($service); break; case 'http_post': $result = $this->check_http_post($service); break; case 'oauth': $result = $this->check_oauth_service($service); break; default: $result = $this->check_http_get($service); } $status = $result['status']; $response_code = $result['response_code']; $error_message = $result['error_message'] ?? ''; } catch (Exception $e) { $status = 'error'; $error_message = $e->getMessage(); } $response_time = round((microtime(true) - $start_time) * 1000, 2); // 转换为毫秒 // 记录检查结果 $this->log_check_result($service->id, $status, $response_time, $response_code, $error_message); // 更新服务状态 $this->update_service_status($service->id, $status, $response_time); return array( 'service_id' => $service->id, 'service_name' => $service->service_name, 'status' => $status, 'response_time' => $response_time, 'response_code' => $response_code, 'error_message' => $error_message ); } /** * HTTP GET方式检查 */ private function check_http_get($service) { $args = array( 'timeout' => 10, 'headers' => $this->prepare_headers($service) ); $response = wp_remote_get($service->api_endpoint, $args); return $this->process_http_response($response); } /** * HTTP POST方式检查 */ private function check_http_post($service) { $args = array( 'timeout' => 10, 'headers' => $this->prepare_headers($service), 'body' => $this->prepare_body($service) ); $response = wp_remote_post($service->api_endpoint, $args); return $this->process_http_response($response); } /** * 准备请求头 */ private function prepare_headers($service) { $headers = array(); // 根据认证方式添加相应的请求头 switch ($service->auth_method) { case 'api_key': if (!empty($service->api_key)) { $headers['Authorization'] = 'Bearer ' . $service->api_key; } break; case 'basic_auth': // 处理基本认证 break; case 'oauth': // 处理OAuth认证 break; } $headers['User-Agent'] = 'WordPress API Monitor/1.0'; return $headers; } /** * 处理HTTP响应 */ private function process_http_response($response) { if (is_wp_error($response)) { return array( 'status' => 'error', 'response_code' => 0, 'error_message' => $response->get_error_message() ); } $response_code = wp_remote_retrieve_response_code($response); if ($response_code >= 200 && $response_code < 300) { $status = 'healthy'; } elseif ($response_code >= 400 && $response_code < 500) { $status = 'warning'; } else { $status = 'error'; } return array( 'status' => $status, 'response_code' => $response_code ); } /** * 记录检查结果到数据库 */ private function log_check_result($service_id, $status, $response_time, $response_code, $error_message) { $this->wpdb->insert( $this->logs_table, array( 'service_id' => $service_id, 'status' => $status, 'response_time' => $response_time, 'response_code' => $response_code, 'error_message' => $error_message ), array('%d', '%s', '%f', '%d', '%s') ); } /** * 更新服务状态 */ private function update_service_status($service_id, $status, $response_time) { // 计算正常运行时间百分比 $uptime_percentage = $this->calculate_uptime_percentage($service_id); $this->wpdb->update( $this->services_table, array( 'last_check' => current_time('mysql'), 'last_status' => $status, 'last_response_time' => $response_time, 'uptime_percentage' => $uptime_percentage ), array('id' => $service_id), array('%s', '%s', '%f', '%f'), array('%d') ); } /** * 计算正常运行时间百分比 */ private function calculate_uptime_percentage($service_id) { $total_checks = $this->wpdb->get_var( $this->wpdb->prepare( "SELECT COUNT(*) FROM {$this->logs_table} WHERE service_id = %d AND check_time > DATE_SUB(NOW(), INTERVAL 7 DAY)", $service_id ) ); if ($total_checks == 0) { return 100; } $healthy_checks = $this->wpdb->get_var( $this->wpdb->prepare( "SELECT COUNT(*) FROM {$this->logs_table} WHERE service_id = %d AND status = 'healthy' AND check_time > DATE_SUB(NOW(), INTERVAL 7 DAY)", $service_id ) ); return round(($healthy_checks / $total_checks) * 100, 2); } } 2.2 实现定时检查功能 为了定期检查API状态,我们需要使用WordPress的定时任务功能。在class-api-monitor-core.php中添加定时任务管理: // 在API_Monitor_Core类中添加以下方法 public function init() { // ... 其他初始化代码 // 初始化定时任务 add_action('amd_check_apis_event', array($this, 'run_api_checks')); // 注册停用插件时的清理操作 register_deactivation_hook(__FILE__, array($this, 'deactivate_plugin')); } public function schedule_events() { if (!wp_next_scheduled('amd_check_apis_event')) { wp_schedule_event(time(), 'five_minutes', 'amd_check_apis_event'); } } public function deactivate_plugin() { // 清除定时任务 wp_clear_scheduled_hook('amd_check_apis_event'); } public function run_api_checks() { require_once AMD_PLUGIN_DIR . 'includes/class-api-checker.php'; $checker = new API_Checker(); $checked_count = $checker->check_all_services(); // 记录检查日志 error_log("API监控:已检查 {$checked_count} 个服务"); } // 添加自定义时间间隔 public function add_cron_interval($schedules) { $schedules['five_minutes'] = array( 'interval' => 300, 'display' => __('每5分钟', 'api-monitor-dashboard') ); $schedules['thirty_minutes'] = array( 'interval' => 1800, 'display' => __('每30分钟', 'api-monitor-dashboard') ); return $schedules; } 第三部分:创建监控管理界面 3.1 设计管理菜单和主面板 现在我们需要创建一个用户友好的管理界面来显示API监控状态。首先在WordPress后台添加菜单项: // 在API_Monitor_Core类中添加 public function add_admin_menu() { add_menu_page( 'API监控面板', 'API监控', 'manage_options', 'api-monitor-dashboard', array($this, 'render_dashboard_page'), 'dashicons-dashboard', 30 ); add_submenu_page( 'api-monitor-dashboard', 'API服务管理', '服务管理', 'manage_options', 'api-monitor-services', array($this, 'render_services_page') ); add_submenu_page( 'api-monitor-dashboard', '监控日志', '监控日志', 'manage_options', 'api-monitor-logs', array($this, 'render_logs_page') ); add_submenu_page( 'api-monitor-dashboard', '设置', '设置', 'manage_options', 'api-monitor-settings', array($this, 'render_settings_page') ); } 3.2 创建仪表板页面 创建templates/dashboard.php文件,用于显示API监控主面板: <div class="wrap"> <h1 class="wp-heading-inline">API监控面板</h1> <div class="amd-dashboard"> <!-- 概览统计 --> <div class="amd-overview-stats"> <div class="amd-stat-card"> <h3>总服务数</h3> <div class="amd-stat-value"><?php echo $total_services; ?></div> </div> <div class="amd-stat-card"> <h3>正常运行</h3> <div class="amd-stat-value amd-stat-healthy"><?php echo $healthy_services; ?></div> </div> <div class="amd-stat-card"> <h3>警告</h3> <div class="amd-stat-value amd-stat-warning"><?php echo $warning_services; ?></div> </div> <div class="amd-stat-card"> <h3>异常</h3> <div class="amd-stat-value amd-stat-error"><?php echo $error_services; ?></div> </div> <div class="amd-stat-card"> <h3>平均响应时间</h3> <div class="amd-stat-value"><?php echo $avg_response_time; ?>ms</div> </div> </div> <!-- API服务状态表格 --> <div class="amd-services-table"> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <th>服务名称</th> <th>类型</th> <th>状态</th> <th>最后检查</th> <th>响应时间</th> <th>操作</th> </tr> </thead> <tbody> <?php foreach ($services as $service): ?> <tr> <td> <strong><?php echo esc_html($service->service_name); ?></strong><br> <small><?php echo esc_html($service->api_endpoint); ?></small> </td> <td><?php echo esc_html($this->get_api_type_label($service->api_type)); ?></td> <td> <?php echo $this->get_status_badge($service->last_status); ?> </td> <td> <?php if ($service->last_check != '0000-00-00 00:00:00') { echo human_time_diff(strtotime($service->last_check), current_time('timestamp')) . '前'; } else { echo '从未检查'; } ?> </td> <td> <?php $response_time_class = ''; if ($service->last_response_time > 1000) { $response_time_class = 'amd-response-slow'; } elseif ($service->last_response_time > 500) { $response_time_class = 'amd-response-medium'; } else { $response_time_class = 'amd-response-fast'; } ?> <span class="<?php echo $response_time_class; ?>"> <?php echo number_format($service->last_response_time, 2); ?>ms </span> </td> <td> <div class="amd-uptime-bar"> <div class="amd-uptime-fill" style="width: <?php echo $service->uptime_percentage; ?>%"></div> <span class="amd-uptime-text"><?php echo $service->uptime_percentage; ?>%</span> </div> </td> <td> <button class="button button-small amd-check-now" data-service-id="<?php echo $service->id; ?>"> 立即检查 </button> <a href="<?php echo admin_url('admin.php?page=api-monitor-services&action=edit&id=' . $service->id); ?>" class="button button-small"> 编辑 </a> </td> </tr> <?php endforeach; ?> </tbody> </table> </div> <!-- 响应时间趋势图 --> <div class="amd-chart-container"> <h3>最近24小时响应时间趋势</h3> <canvas id="amd-response-time-chart" width="800" height="200"></canvas> </div> <!-- 状态分布图 --> <div class="amd-status-distribution"> <h3>服务状态分布</h3> <div class="amd-status-chart-container"> <canvas id="amd-status-chart" width="400" height="400"></canvas> </div> </div> </div> </div> ### 3.3 创建服务管理页面 创建`templates/services.php`,用于管理API服务配置: <div class="wrap"> <h1 class="wp-heading-inline">API服务管理</h1> <a href="<?php echo admin_url('admin.php?page=api-monitor-services&action=add'); ?>" class="page-title-action">添加新服务</a> <?php if ($action == 'list'): ?> <div class="tablenav top"> <div class="alignleft actions"> <select name="bulk-action"> <option value="-1">批量操作</option> <option value="activate">启用</option> <option value="deactivate">停用</option> <option value="delete">删除</option> </select> <input type="submit" class="button action" value="应用"> </div> <div class="tablenav-pages"> <span class="displaying-num"><?php echo $total_items; ?>个项目</span> </div> </div> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <td class="manage-column column-cb check-column"> <input type="checkbox"> </td> <th>服务名称</th> <th>API端点</th> <th>类型</th> <th>状态</th> <th>检查间隔</th> <th>最后检查</th> <th>操作</th> </tr> </thead> <tbody> <?php foreach ($services as $service): ?> <tr> <th scope="row" class="check-column"> <input type="checkbox" name="service_ids[]" value="<?php echo $service->id; ?>"> </th> <td> <strong> <a href="<?php echo admin_url('admin.php?page=api-monitor-services&action=edit&id=' . $service->id); ?>"> <?php echo esc_html($service->service_name); ?> </a> </strong> </td> <td><?php echo esc_html($this->truncate_string($service->api_endpoint, 50)); ?></td> <td><?php echo esc_html($service->api_type); ?></td> <td> <?php if ($service->is_active): ?> <span class="amd-status-active">已启用</span> <?php else: ?> <span class="amd-status-inactive">已停用</span> <?php endif; ?> </td> <td><?php echo $this->format_interval($service->check_interval); ?></td> <td> <?php if ($service->last_check != '0000-00-00 00:00:00') { echo date('Y-m-d H:i:s', strtotime($service->last_check)); } else { echo '从未检查'; } ?> </td> <td> <a href="<?php echo admin_url('admin.php?page=api-monitor-services&action=edit&id=' . $service->id); ?>">编辑</a> | <a href="<?php echo admin_url('admin.php?page=api-monitor-services&action=delete&id=' . $service->id); ?>" onclick="return confirm('确定要删除这个服务吗?')">删除</a> | <?php if ($service->is_active): ?> <a href="<?php echo admin_url('admin.php?page=api-monitor-services&action=deactivate&id=' . $service->id); ?>">停用</a> <?php else: ?> <a href="<?php echo admin_url('admin.php?page=api-monitor-services&action=activate&id=' . $service->id); ?>">启用</a> <?php endif; ?> </td> </tr> <?php endforeach; ?> </tbody> </table> <?php elseif ($action == 'add' || $action == 'edit'): ?> <form method="post" action=""> <?php wp_nonce_field('amd_save_service', 'amd_service_nonce'); ?> <table class="form-table"> <tr> <th scope="row"><label for="service_name">服务名称</label></th> <td> <input type="text" id="service_name" name="service_name" value="<?php echo esc_attr($service_data['service_name'] ?? ''); ?>" class="regular-text" required> <p class="description">显示在监控面板中的服务名称</p> </td> </tr> <tr> <th scope="row"><label for="api_endpoint">API端点</label></th> <td> <input type="url" id="api_endpoint" name="api_endpoint" value="<?php echo esc_attr($service_data['api_endpoint'] ?? ''); ?>" class="regular-text" required> <p class="description">API的完整URL地址</p> </td> </tr> <tr> <th scope="row"><label for="api_type">API类型</label></th> <td> <select id="api_type" name="api_type" class="regular-text"> <option value="http_get" <?php selected($service_data['api_type'] ?? '', 'http_get'); ?>>HTTP GET</option> <option value="http_post" <?php selected($service_data['api_type'] ?? '', 'http_post'); ?>>HTTP POST</option> <option value="oauth" <?php selected($service_data['api_type'] ?? '', 'oauth'); ?>>OAuth</option> <option value="websocket" <?php selected($service_data['api_type'] ?? '', 'websocket'); ?>>WebSocket</option> <option value="custom" <?php selected($service_data['api_type'] ?? '', 'custom'); ?>>自定义</option> </select> </td> </tr> <tr> <th scope="row"><label for="auth_method">认证方式</label></th> <td> <select id="auth_method" name="auth_method" class="regular-text"> <option value="none" <?php selected($service_data['auth_method'] ?? '', 'none'); ?>>无需认证</option> <option value="api_key" <?php selected($service_data['auth_method'] ?? '', 'api_key'); ?>>API密钥</option> <option value="basic_auth" <?php selected($service_data['auth_method'] ?? '', 'basic_auth'); ?>>基本认证</option> <option value="oauth" <?php selected($service_data['auth_method'] ?? '', 'oauth'); ?>>OAuth 2.0</option> </select> </td> </tr> <tr class="auth-field" id="api_key_field"> <th scope="row"><label for="api_key">API密钥</label></th> <td> <input type="password" id="api_key" name="api_key" value="<?php echo esc_attr($service_data['api_key'] ?? ''); ?>" class="regular-text"> <button type="button" class="button button-small toggle-password">显示/隐藏</button> <p class="description">API访问密钥,将安全存储</p> </td> </tr> <tr> <th scope="row"><label for="check_interval">检查间隔</label></th> <td> <select id="check_interval" name="check_interval" class="regular-text"> <option value="60" <?php selected($service_data['check_interval'] ?? 300, 60); ?>>1分钟</option> <option value="300" <?php selected($service_data['check_interval'] ?? 300, 300); ?>>5分钟</option> <option value="900" <?php selected($service_data['check_interval'] ?? 300, 900); ?>>15分钟</option> <option value="1800" <?php selected($service_data['check_interval'] ?? 300, 1800); ?>>30分钟</option> <option value="3600" <?php selected($service_data['check_interval'] ?? 300, 3600); ?>>1小时</option> </select> </td> </tr> <tr> <th scope="row"><label for="expected_response">预期响应</label></th> <td> <textarea id="expected_response" name="expected_response" rows="3" class="large-text"><?php echo esc_textarea($service_data['expected_response'] ?? ''); ?></textarea> <p class="description">可选的预期响应内容(用于验证API返回正确数据)</p> </td> </tr> <tr> <th scope="row"><label for="is_active">状态</label></th> <td> <label> <input type="checkbox" id="is_active" name="is_active" value="1" <?php checked($service_data['is_active'] ?? 1, 1); ?>> 启用此服务的监控 </label> </td> </tr> </table> <p class="submit"> <input type="submit" name="submit" class="button button-primary" value="保存服务"> <a href="<?php echo admin_url('admin.php?page=api-monitor-services'); ?>" class="button">取消</a> </p> </form> <?php endif; ?> </div> ## 第四部分:增强功能与互联网小工具集成 ### 4.1 实现实时通知功能 为了让管理员及时了解API状态变化,我们需要实现通知功能。创建`includes/class-notification-manager.php`: class Notification_Manager { private $notification_methods = array(); public function __construct() { // 初始化通知方法 $this->notification_methods = array( 'email' => array($this, 'send_email_notification'), 'slack' => array($this, 'send_slack_notification'), 'webhook' => array($this, 'send_webhook_notification'), 'sms' => array($this, 'send_sms_notification') ); } /** * 发送状态变化通知 */ public function send_status_change_notification($service_id, $old_status, $new_status, $service_data) { $notification_settings = get_option('amd_notification_settings', array()); // 检查是否需要发送通知 if (!$this->should_send_notification($old_status, $new_status, $notification_settings)) { return; } // 准备通知内容 $message = $this->prepare_notification_message($service_data, $old_status, $new_status); // 发送到所有启用的通知渠道 foreach ($notification_settings['channels'] as $channel => $enabled) { if ($enabled && isset($this->notification_methods[$channel])) { call_user_func($this->notification_methods[$channel], $message, $service_data); } } } /** * 准备通知消息 */ private function prepare_notification_message($service_data, $old_status, $new_status) { $site_name = get_bloginfo('name'); $timestamp = current_time('mysql'); $status_labels = array( 'healthy' => '正常', 'warning' => '警告', 'error' => '异常', 'unknown' => '未知' ); $message = "【{$site_name} API监控通知】nn"; $message .= "服务名称:{$service_data['service_name']}n"; $message .= "API端点:{$service_data['api_endpoint']}n"; $message .= "状态变化:{$status_labels[$old_status]} → {$status_labels[$new_status]}n"; $message .= "发生时间:{$timestamp}n"; if ($new_status == 'error' && !empty($service_data['error_message'])) { $message .= "错误信息:{$service_data['error_message']}n"; } $message .= "n请及时处理。"; return $message; } /** * 发送邮件通知 */ private function send_email_notification($message, $service_data) { $settings = get_option('amd_notification_settings', array()); $to = $settings['email_recipients'] ?? get_option('admin_email'); $subject = "API监控告警:{$service_data['service_name']} 状态异常"; wp_mail($to, $subject, $message); } /** * 发送Slack通知 */ private function send_slack_notification($message, $service_data) { $settings = get_option('amd_notification_settings', array()); if (empty($settings['slack_webhook_url'])) { return; } $payload = array( 'text' => $message, 'username' => 'API监控机器人', 'icon_emoji' => ':warning:', 'attachments' => array( array( 'color' => $this->get_status_color($service_data['last_status']), 'fields' => array( array( 'title' => '服务名称', 'value' => $service_data['service_name'], 'short' => true ), array( 'title' => '当前状态', 'value' => $service_data['last_status'], 'short' => true ) ) ) ) ); $args = array( 'body' => json_encode($payload), 'headers' => array('Content-Type' => 'application/json'), 'timeout' => 5 ); wp_remote_post($settings['slack_webhook_url'], $args); } /** * 获取状态对应的颜色 */ private function get_status_color($status) { $colors = array( 'healthy' => '#36a64f', 'warning' => '#ffcc00', 'error' => '#ff0000', 'unknown' => '#cccccc' ); return $colors[$status] ?? '#cccccc'; } } ### 4.2 集成常用互联网小工具 #### 4.2.1 天气信息小工具 创建`includes/widgets/class-weather-widget.php`: class Weather_Widget extends WP_Widget { public function __construct() { parent::__construct( 'amd_weather_widget', 'API监控 - 天气信息', array('description' => '显示当前天气信息') ); } public function widget($args, $instance) { $weather_data = $this->get_weather_data($instance['city']); echo $args['before_widget']; if (!empty($instance['title'])) { echo $args['before_title'] . apply_filters('widget_title', $instance['title']) . $args['after_title']; } if ($weather_data) { echo '<div class="amd

发表评论

详细教程,为网站打造内嵌的在线简易视频字幕生成与编辑工具

详细教程:为WordPress网站打造内嵌在线简易视频字幕生成与编辑工具 引言:为什么网站需要内置视频字幕工具? 在当今多媒体内容主导的互联网环境中,视频已成为网站吸引和保留用户的重要手段。然而,许多网站运营者面临一个共同挑战:如何高效地为视频内容添加字幕。字幕不仅有助于听力障碍用户访问内容,还能提高视频在静音环境下的观看率,对SEO优化也有显著帮助。 传统上,为视频添加字幕需要依赖第三方工具或专业软件,过程繁琐且耗时。本文将详细介绍如何通过WordPress代码二次开发,为您的网站打造一个内嵌的在线简易视频字幕生成与编辑工具,让字幕创建变得简单高效。 第一部分:项目规划与技术选型 1.1 功能需求分析 在开始开发之前,我们需要明确工具的核心功能: 视频上传与预览:支持常见视频格式上传和实时预览 自动语音识别(ASR):将视频中的语音转换为文字 字幕编辑界面:直观的时间轴编辑功能 字幕格式支持:至少支持SRT、VTT等常用字幕格式 实时预览:编辑字幕时可实时查看效果 导出与集成:将生成的字幕与视频关联或单独导出 1.2 技术架构设计 我们将采用以下技术栈: 前端:HTML5、CSS3、JavaScript(使用Vue.js框架) 视频处理:HTML5 Video API + FFmpeg.wasm(浏览器端处理) 语音识别:Web Speech API(免费基础方案)或集成第三方API(如Google Cloud Speech-to-Text) 后端:WordPress PHP环境,使用REST API处理文件操作 存储:WordPress媒体库 + 自定义数据库表存储字幕数据 1.3 开发环境准备 确保您的WordPress开发环境满足以下条件: WordPress 5.0+(支持REST API) PHP 7.4+(支持最新语法特性) 至少256MB内存限制(用于处理视频文件) 启用文件上传功能(支持视频格式) 安装并启用必要的开发插件(如Query Monitor、Debug Bar) 第二部分:创建WordPress插件框架 2.1 初始化插件结构 首先,在WordPress的wp-content/plugins/目录下创建新文件夹video-subtitle-tool,并创建以下基础文件结构: video-subtitle-tool/ ├── video-subtitle-tool.php # 主插件文件 ├── includes/ │ ├── class-database.php # 数据库处理类 │ ├── class-video-processor.php # 视频处理类 │ ├── class-subtitle-generator.php # 字幕生成类 │ └── class-api-handler.php # REST API处理类 ├── admin/ │ ├── css/ │ │ └── admin-style.css # 后台样式 │ ├── js/ │ │ └── admin-script.js # 后台脚本 │ └── admin-page.php # 管理页面 ├── public/ │ ├── css/ │ │ └── public-style.css # 前端样式 │ ├── js/ │ │ ├── app.js # 主Vue应用 │ │ ├── video-processor.js # 视频处理逻辑 │ │ └── subtitle-editor.js # 字幕编辑器 │ └── shortcode.php # 短代码处理 ├── assets/ │ ├── ffmpeg/ │ │ └── ffmpeg-core.js # FFmpeg.wasm核心文件 │ └── icons/ # 图标资源 └── templates/ └── subtitle-editor.php # 编辑器模板 2.2 主插件文件配置 编辑video-subtitle-tool.php,添加插件基本信息: <?php /** * Plugin Name: 视频字幕生成与编辑工具 * Plugin URI: https://yourwebsite.com/video-subtitle-tool * Description: 为WordPress网站添加内嵌的在线视频字幕生成与编辑功能 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later * Text Domain: video-subtitle-tool */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('VST_VERSION', '1.0.0'); define('VST_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('VST_PLUGIN_URL', plugin_dir_url(__FILE__)); define('VST_MAX_FILE_SIZE', 500 * 1024 * 1024); // 500MB限制 // 自动加载类文件 spl_autoload_register(function ($class) { $prefix = 'VST_'; $base_dir = VST_PLUGIN_DIR . 'includes/'; $len = strlen($prefix); if (strncmp($prefix, $class, $len) !== 0) { return; } $relative_class = substr($class, $len); $file = $base_dir . 'class-' . str_replace('_', '-', strtolower($relative_class)) . '.php'; if (file_exists($file)) { require $file; } }); // 初始化插件 function vst_init() { // 检查依赖 if (!function_exists('wp_get_current_user')) { require_once(ABSPATH . 'wp-includes/pluggable.php'); } // 初始化数据库 VST_Database::init(); // 注册短代码 add_shortcode('video_subtitle_editor', array('VST_Shortcode', 'render_editor')); // 初始化REST API add_action('rest_api_init', array('VST_API_Handler', 'register_routes')); // 加载文本域 load_plugin_textdomain('video-subtitle-tool', false, dirname(plugin_basename(__FILE__)) . '/languages/'); } add_action('plugins_loaded', 'vst_init'); // 激活插件时执行 function vst_activate() { require_once VST_PLUGIN_DIR . 'includes/class-database.php'; VST_Database::create_tables(); // 设置默认选项 add_option('vst_default_language', 'zh-CN'); add_option('vst_max_video_duration', 3600); // 默认最大1小时 add_option('vst_enable_auto_generation', true); } register_activation_hook(__FILE__, 'vst_activate'); // 停用插件时清理 function vst_deactivate() { // 清理临时文件 VST_Video_Processor::cleanup_temp_files(); } register_deactivation_hook(__FILE__, 'vst_deactivate'); 第三部分:数据库设计与实现 3.1 创建数据库表 编辑includes/class-database.php: <?php class VST_Database { public static function init() { // 数据库版本 $db_version = '1.0'; $current_version = get_option('vst_db_version', '0'); if ($current_version !== $db_version) { self::create_tables(); update_option('vst_db_version', $db_version); } } public static function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_prefix = $wpdb->prefix . 'vst_'; // 视频项目表 $projects_table = $table_prefix . 'projects'; $projects_sql = "CREATE TABLE IF NOT EXISTS $projects_table ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, title varchar(255) NOT NULL, video_url varchar(1000) NOT NULL, video_duration int(11) DEFAULT 0, language varchar(10) DEFAULT 'zh-CN', status varchar(20) DEFAULT 'draft', 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 status (status) ) $charset_collate;"; // 字幕表 $subtitles_table = $table_prefix . 'subtitles'; $subtitles_sql = "CREATE TABLE IF NOT EXISTS $subtitles_table ( id bigint(20) NOT NULL AUTO_INCREMENT, project_id bigint(20) NOT NULL, start_time int(11) NOT NULL, end_time int(11) NOT NULL, text text NOT NULL, confidence float DEFAULT 1.0, is_edited tinyint(1) DEFAULT 0, sequence int(11) NOT NULL, PRIMARY KEY (id), KEY project_id (project_id), KEY sequence (sequence) ) $charset_collate;"; // 项目设置表 $settings_table = $table_prefix . 'project_settings'; $settings_sql = "CREATE TABLE IF NOT EXISTS $settings_table ( project_id bigint(20) NOT NULL, font_family varchar(100) DEFAULT 'Arial', font_size int(11) DEFAULT 24, font_color varchar(7) DEFAULT '#FFFFFF', background_color varchar(7) DEFAULT '#00000080', position varchar(20) DEFAULT 'bottom', max_lines int(11) DEFAULT 2, UNIQUE KEY project_id (project_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($projects_sql); dbDelta($subtitles_sql); dbDelta($settings_sql); } public static function cleanup_temp_files() { // 清理超过24小时的临时文件 $temp_dir = wp_upload_dir()['basedir'] . '/vst_temp/'; if (is_dir($temp_dir)) { $files = glob($temp_dir . '*'); $now = time(); foreach ($files as $file) { if (is_file($file)) { if ($now - filemtime($file) >= 86400) { // 24小时 unlink($file); } } } } } } 第四部分:视频处理与语音识别模块 4.1 视频处理类实现 创建includes/class-video-processor.php: <?php class VST_Video_Processor { /** * 处理上传的视频文件 */ public static function process_upload($file) { // 安全检查 if (!self::validate_file($file)) { return new WP_Error('invalid_file', '无效的视频文件'); } // 创建临时目录 $upload_dir = wp_upload_dir(); $temp_dir = $upload_dir['basedir'] . '/vst_temp/'; if (!is_dir($temp_dir)) { wp_mkdir_p($temp_dir); } // 生成唯一文件名 $file_name = sanitize_file_name($file['name']); $unique_name = wp_unique_filename($temp_dir, $file_name); $temp_path = $temp_dir . $unique_name; // 移动文件 if (!move_uploaded_file($file['tmp_name'], $temp_path)) { return new WP_Error('upload_failed', '文件上传失败'); } // 获取视频信息 $video_info = self::get_video_info($temp_path); return array( 'path' => $temp_path, 'url' => $upload_dir['baseurl'] . '/vst_temp/' . $unique_name, 'name' => $file_name, 'info' => $video_info ); } /** * 验证文件 */ private static function validate_file($file) { $allowed_types = array('mp4', 'avi', 'mov', 'wmv', 'flv', 'webm'); $file_ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); // 检查文件类型 if (!in_array($file_ext, $allowed_types)) { return false; } // 检查文件大小 $max_size = get_option('vst_max_file_size', VST_MAX_FILE_SIZE); if ($file['size'] > $max_size) { return false; } // 检查MIME类型 $finfo = finfo_open(FILEINFO_MIME_TYPE); $mime_type = finfo_file($finfo, $file['tmp_name']); finfo_close($finfo); $allowed_mimes = array( 'video/mp4', 'video/x-msvideo', 'video/quicktime', 'video/x-ms-wmv', 'video/x-flv', 'video/webm' ); return in_array($mime_type, $allowed_mimes); } /** * 获取视频信息 */ private static function get_video_info($file_path) { // 使用FFprobe获取视频信息(需要服务器安装FFmpeg) if (function_exists('shell_exec')) { $cmd = "ffprobe -v quiet -print_format json -show_format -show_streams " . escapeshellarg($file_path); $output = shell_exec($cmd); if ($output) { $info = json_decode($output, true); if ($info) { return array( 'duration' => floatval($info['format']['duration']), 'width' => intval($info['streams'][0]['width']), 'height' => intval($info['streams'][0]['height']), 'bitrate' => intval($info['format']['bit_rate']), 'format' => $info['format']['format_name'] ); } } } // 备用方案:使用PHP的getid3库 if (!class_exists('getID3')) { require_once(ABSPATH . 'wp-admin/includes/media.php'); } $id3 = new getID3(); $file_info = $id3->analyze($file_path); return array( 'duration' => isset($file_info['playtime_seconds']) ? $file_info['playtime_seconds'] : 0, 'width' => isset($file_info['video']['resolution_x']) ? $file_info['video']['resolution_x'] : 0, 'height' => isset($file_info['video']['resolution_y']) ? $file_info['video']['resolution_y'] : 0, 'bitrate' => isset($file_info['bitrate']) ? $file_info['bitrate'] : 0, 'format' => isset($file_info['fileformat']) ? $file_info['fileformat'] : 'unknown' ); } /** * 提取音频用于语音识别 */ public static function extract_audio($video_path, $output_path = null) { if (!$output_path) { $upload_dir = wp_upload_dir(); $output_path = $upload_dir['basedir'] . '/vst_temp/audio_' . uniqid() . '.wav'; } // 使用FFmpeg提取音频 $cmd = "ffmpeg -i " . escapeshellarg($video_path) . " -ac 1 -ar 16000 -vn " . escapeshellarg($output_path) . " 2>&1"; exec($cmd, $output, $return_code); if ($return_code === 0 && file_exists($output_path)) { return $output_path; } return false; } } 4.2 语音识别集成 创建includes/class-subtitle-generator.php: <?php class VST_Subtitle_Generator { /** * 使用Web Speech API进行语音识别(客户端方案) */ public static function get_browser_recognition_code() { // 返回客户端JavaScript代码 return ' <script> class SpeechRecognitionTool { constructor() { this.recognition = null; this.isRecording = false; this.transcript = ""; this.interimResults = []; this.init(); } init() { const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; if (!SpeechRecognition) { throw new Error("浏览器不支持语音识别API"); } this.recognition = new SpeechRecognition(); this.recognition.continuous = true; this.recognition.interimResults = true; this.recognition.lang = "zh-CN"; this.recognition.onresult = (event) => { let interimTranscript = ""; let finalTranscript = ""; for (let i = event.resultIndex; i < event.results.length; i++) { const transcript = event.results[i][0].transcript; if (event.results[i].isFinal) { finalTranscript += transcript; } else { interimTranscript += transcript; } } this.interimResults = interimTranscript; this.transcript += finalTranscript; // 触发自定义事件 const transcriptEvent = new CustomEvent("transcriptUpdate", { detail: { final: finalTranscript, interim: interimTranscript, full: this.transcript } }); document.dispatchEvent(transcriptEvent); }; this.recognition.onerror = (event) => { console.error("语音识别错误:", event.error); }; } start() { if (this.recognition && !this.isRecording) { this.recognition.start(); this.isRecording = true; } } stop() { if (this.recognition && this.isRecording) { this.recognition.stop(); this.isRecording = false; } } 第四部分:视频处理与语音识别模块(续) 4.2 语音识别集成(续) setLanguage(lang) { if (this.recognition) { this.recognition.lang = lang; } } getTranscript() { return this.transcript; } reset() { this.transcript = ""; this.interimResults = ""; } } // 导出到全局 window.VSTSpeechRecognition = SpeechRecognitionTool; </script> '; } /** * 使用第三方API进行语音识别(服务器端方案) */ public static function recognize_with_api($audio_path, $language = 'zh-CN') { $api_type = get_option('vst_speech_api', 'google'); switch ($api_type) { case 'google': return self::google_speech_recognition($audio_path, $language); case 'azure': return self::azure_speech_recognition($audio_path, $language); case 'baidu': return self::baidu_speech_recognition($audio_path, $language); default: return new WP_Error('unsupported_api', '不支持的语音识别API'); } } /** * Google Cloud Speech-to-Text */ private static function google_speech_recognition($audio_path, $language) { $api_key = get_option('vst_google_api_key', ''); if (empty($api_key)) { return new WP_Error('missing_api_key', '未配置Google API密钥'); } // 读取音频文件 $audio_content = file_get_contents($audio_path); $base64_audio = base64_encode($audio_content); $url = "https://speech.googleapis.com/v1/speech:recognize?key=" . $api_key; $data = array( 'config' => array( 'encoding' => 'LINEAR16', 'sampleRateHertz' => 16000, 'languageCode' => $language, 'enableWordTimeOffsets' => true, 'enableAutomaticPunctuation' => true ), 'audio' => array( 'content' => $base64_audio ) ); $response = wp_remote_post($url, array( 'headers' => array('Content-Type' => 'application/json'), 'body' => json_encode($data), 'timeout' => 60 )); if (is_wp_error($response)) { return $response; } $body = json_decode(wp_remote_retrieve_body($response), true); if (isset($body['error'])) { return new WP_Error('api_error', $body['error']['message']); } return self::format_recognition_results($body['results'], 'google'); } /** * 格式化识别结果 */ private static function format_recognition_results($results, $provider) { $subtitles = array(); $current_time = 0; foreach ($results as $result) { if (!isset($result['alternatives'][0])) { continue; } $alternative = $result['alternatives'][0]; $text = $alternative['transcript']; if ($provider === 'google' && isset($alternative['words'])) { // 基于词语时间戳分割字幕 $words = $alternative['words']; $chunks = self::split_into_subtitles($words); foreach ($chunks as $chunk) { $subtitles[] = array( 'start' => $chunk['start'], 'end' => $chunk['end'], 'text' => $chunk['text'], 'confidence' => $alternative['confidence'] ?? 0.8 ); } } else { // 简单分割:每5秒一句 $duration = 5; // 每句5秒 $sentences = self::split_text_into_sentences($text); foreach ($sentences as $sentence) { $subtitles[] = array( 'start' => $current_time, 'end' => $current_time + $duration, 'text' => $sentence, 'confidence' => 0.7 ); $current_time += $duration; } } } return $subtitles; } /** * 将词语分割成字幕块 */ private static function split_into_subtitles($words, $max_chars = 40) { $chunks = array(); $current_chunk = array( 'text' => '', 'start' => null, 'end' => null, 'word_count' => 0 ); foreach ($words as $word) { $word_text = $word['word']; $word_start = floatval($word['startTime']['seconds']) + (floatval($word['startTime']['nanos'] ?? 0) / 1000000000); $word_end = floatval($word['endTime']['seconds']) + (floatval($word['endTime']['nanos'] ?? 0) / 1000000000); // 如果当前块为空,设置开始时间 if ($current_chunk['start'] === null) { $current_chunk['start'] = $word_start; } // 检查是否应该开始新块 if (strlen($current_chunk['text'] . ' ' . $word_text) > $max_chars || $current_chunk['word_count'] >= 8) { // 保存当前块 if (!empty($current_chunk['text'])) { $current_chunk['end'] = $word_start; // 使用下一个词的开始时间作为结束 $chunks[] = $current_chunk; } // 开始新块 $current_chunk = array( 'text' => $word_text, 'start' => $word_start, 'end' => $word_end, 'word_count' => 1 ); } else { // 添加到当前块 $current_chunk['text'] .= (empty($current_chunk['text']) ? '' : ' ') . $word_text; $current_chunk['end'] = $word_end; $current_chunk['word_count']++; } } // 添加最后一个块 if (!empty($current_chunk['text'])) { $chunks[] = $current_chunk; } return $chunks; } /** * 将文本分割成句子 */ private static function split_text_into_sentences($text) { // 简单的句子分割逻辑 $sentences = preg_split('/(?<=[。!?.!?])/u', $text, -1, PREG_SPLIT_NO_EMPTY); return array_filter(array_map('trim', $sentences)); } } 第五部分:前端编辑器开发 5.1 编辑器HTML结构 创建templates/subtitle-editor.php: <div id="vst-editor-app" class="vst-editor-container"> <!-- 顶部工具栏 --> <div class="vst-toolbar"> <div class="vst-toolbar-left"> <button class="vst-btn vst-btn-primary" @click="uploadVideo"> <i class="vst-icon-upload"></i> 上传视频 </button> <button class="vst-btn" :class="{'vst-btn-active': isRecording}" @click="toggleRecording"> <i class="vst-icon-mic"></i> {{ isRecording ? '停止识别' : '开始识别' }} </button> <select v-model="selectedLanguage" class="vst-select"> <option value="zh-CN">中文(简体)</option> <option value="zh-TW">中文(繁体)</option> <option value="en-US">English</option> <option value="ja-JP">日本語</option> <option value="ko-KR">한국어</option> </select> </div> <div class="vst-toolbar-right"> <button class="vst-btn" @click="exportSubtitles('srt')"> <i class="vst-icon-download"></i> 导出SRT </button> <button class="vst-btn" @click="exportSubtitles('vtt')"> <i class="vst-icon-download"></i> 导出VTT </button> <button class="vst-btn vst-btn-success" @click="saveProject"> <i class="vst-icon-save"></i> 保存项目 </button> </div> </div> <!-- 主编辑区 --> <div class="vst-main-editor"> <!-- 视频播放器 --> <div class="vst-video-container"> <video ref="videoPlayer" :src="videoUrl" controls @timeupdate="updateCurrentTime" @loadedmetadata="onVideoLoaded" ></video> <div class="vst-video-overlay" v-if="subtitles.length > 0"> <div class="vst-subtitle-display" :style="subtitleStyle"> {{ currentSubtitleText }} </div> </div> </div> <!-- 字幕编辑区 --> <div class="vst-subtitle-editor"> <div class="vst-subtitle-header"> <h3>字幕编辑</h3> <div class="vst-subtitle-actions"> <button class="vst-btn vst-btn-small" @click="addSubtitle"> <i class="vst-icon-add"></i> 添加字幕 </button> <button class="vst-btn vst-btn-small" @click="autoSync"> <i class="vst-icon-sync"></i> 自动同步 </button> </div> </div> <!-- 字幕列表 --> <div class="vst-subtitle-list"> <div v-for="(subtitle, index) in subtitles" :key="index" class="vst-subtitle-item" :class="{'vst-subtitle-active': currentSubtitleIndex === index}" @click="selectSubtitle(index)" > <div class="vst-subtitle-index">{{ index + 1 }}</div> <div class="vst-subtitle-time"> <input type="number" v-model="subtitle.start" step="0.1" @change="updateSubtitleTime(index, 'start', $event)" > <span> → </span> <input type="number" v-model="subtitle.end" step="0.1" @change="updateSubtitleTime(index, 'end', $event)" > </div> <div class="vst-subtitle-text"> <textarea v-model="subtitle.text" @input="updateSubtitleText(index, $event)" rows="2" ></textarea> </div> <div class="vst-subtitle-actions"> <button class="vst-btn-icon" @click="playSegment(index)"> <i class="vst-icon-play"></i> </button> <button class="vst-btn-icon" @click="removeSubtitle(index)"> <i class="vst-icon-delete"></i> </button> </div> </div> </div> <!-- 时间轴编辑器 --> <div class="vst-timeline-editor"> <div class="vst-timeline-header"> <span>时间轴</span> <span class="vst-current-time">{{ formatTime(currentTime) }}</span> </div> <div class="vst-timeline-container" ref="timeline"> <div v-for="(subtitle, index) in subtitles" :key="'timeline-' + index" class="vst-timeline-segment" :style="{ left: (subtitle.start / videoDuration * 100) + '%', width: ((subtitle.end - subtitle.start) / videoDuration * 100) + '%' }" @mousedown="startDrag(index, $event)" > <div class="vst-timeline-label">{{ index + 1 }}</div> </div> <div class="vst-timeline-cursor" :style="{ left: (currentTime / videoDuration * 100) + '%' }" ></div> </div> </div> </div> </div> <!-- 设置面板 --> <div class="vst-settings-panel" v-if="showSettings"> <div class="vst-settings-header"> <h3>字幕设置</h3> <button class="vst-btn-icon" @click="showSettings = false"> <i class="vst-icon-close"></i> </button> </div> <div class="vst-settings-content"> <div class="vst-setting-item"> <label>字体</label> <select v-model="settings.fontFamily"> <option value="Arial">Arial</option> <option value="Microsoft YaHei">微软雅黑</option> <option value="SimSun">宋体</option> <option value="SimHei">黑体</option> </select> </div> <div class="vst-setting-item"> <label>字体大小</label> <input type="range" v-model="settings.fontSize" min="12" max="48"> <span>{{ settings.fontSize }}px</span> </div> <div class="vst-setting-item"> <label>字体颜色</label> <input type="color" v-model="settings.fontColor"> </div> <div class="vst-setting-item"> <label>背景颜色</label> <input type="color" v-model="settings.backgroundColor"> <input type="range" v-model="settings.backgroundOpacity" min="0" max="100"> <span>{{ settings.backgroundOpacity }}%</span> </div> <div class="vst-setting-item"> <label>位置</label> <select v-model="settings.position"> <option value="bottom">底部</option> <option value="top">顶部</option> <option value="middle">中间</option> </select> </div> </div> </div> <!-- 上传模态框 --> <div class="vst-modal" v-if="showUploadModal"> <div class="vst-modal-content"> <div class="vst-modal-header"> <h3>上传视频</h3> <button class="vst-btn-icon" @click="showUploadModal = false"> <i class="vst-icon-close"></i> </button> </div> <div class="vst-modal-body"> <div class="vst-upload-area" @dragover.prevent="onDragOver" @dragleave.prevent="onDragLeave" @drop.prevent="onDrop" :class="{'vst-upload-dragover': isDragOver}" > <i class="vst-icon-upload-large"></i> <p>拖放视频文件到这里,或</p> <input type="file" ref="fileInput" accept="video/*" @change="onFileSelected" hidden > <button class="vst-btn" @click="$refs.fileInput.click()"> 选择文件 </button> <p class="vst-upload-hint">支持 MP4, AVI, MOV, WMV, FLV, WebM 格式,最大500MB</p> </div> <div v-if="uploadProgress > 0" class="vst-upload-progress"> <div class="vst-progress-bar"> <div class="vst-progress-fill" :style="{ width: uploadProgress + '%' }"></div> </div> <span>{{ uploadProgress }}%</span> </div> </div> </div> </div> </div> 5.2 编辑器Vue.js应用 创建public/js/app.js: // Vue.js应用主文件 document.addEventListener('DOMContentLoaded', function() { // 检查Vue是否已加载 if (typeof Vue === 'undefined') { console.error('Vue.js未加载,请确保已引入Vue库'); return; } // 创建Vue应用 const app = Vue.createApp({ data() { return { // 视频相关 videoUrl: '', videoDuration: 0, currentTime: 0, videoFile: null, // 字幕数据 subtitles: [], currentSubtitleIndex: -1, currentSubtitleText: '', // 识别状态 isRecording: false, selectedLanguage: 'zh-CN', recognitionTool: null, // 界面状态 showSettings: false, showUploadModal: false, isDragOver: false, uploadProgress: 0, // 设置 settings: { fontFamily: 'Microsoft YaHei', fontSize: 24, fontColor: '#FFFFFF', backgroundColor: '#000000', backgroundOpacity: 50, position: 'bottom' }, // 项目信息 projectId: null, projectTitle: '未命名项目', isSaved: false }; }, computed: { // 计算字幕显示样式 subtitleStyle() { const bgColor = this.hexToRgba( this.settings.backgroundColor, this.settings.backgroundOpacity / 100 );

发表评论

WordPress高级教程,开发集成在线问卷调查结果实时数据看板

WordPress高级教程:开发集成在线问卷调查结果实时数据看板 引言:WordPress作为企业级应用开发平台 WordPress早已超越了简单的博客系统定位,如今已成为一个功能强大的内容管理系统(CMS)和应用程序框架。全球超过43%的网站使用WordPress构建,这得益于其灵活的可扩展性和庞大的开发者社区。在本教程中,我们将探索如何通过WordPress代码二次开发,实现一个专业级的在线问卷调查结果实时数据看板,展示如何将WordPress转变为功能丰富的互联网应用平台。 传统的问卷调查工具往往独立于企业网站存在,导致数据孤岛和用户体验割裂。通过将问卷调查与数据可视化看板直接集成到WordPress中,我们可以创建无缝的用户体验,同时利用WordPress的用户管理、权限控制和内容展示能力。本教程将引导您完成从需求分析到代码实现的完整过程,适合有一定WordPress开发经验的开发者。 第一部分:项目架构设计与技术选型 1.1 需求分析与功能规划 在开始编码之前,我们需要明确项目的核心需求: 问卷调查功能:支持多种题型(单选、多选、矩阵、评分等) 实时数据收集:用户提交问卷后立即更新数据存储 可视化看板:多种图表展示调查结果(柱状图、饼图、趋势图等) 权限管理:不同用户角色查看不同级别的数据 响应式设计:在桌面和移动设备上都能良好显示 数据导出:支持将结果导出为CSV、PDF等格式 1.2 技术栈选择 基于WordPress生态,我们选择以下技术方案: 核心框架:WordPress 5.8+,使用自定义文章类型存储问卷和结果 前端框架:Vue.js 3.0 + Chart.js,实现交互式数据可视化 实时通信:REST API + WebSocket(可选,用于实时更新) 数据库优化:自定义数据库表结构,提高查询效率 缓存机制:Transients API + Object Cache,提升性能 1.3 系统架构设计 我们将采用分层架构设计: 表现层 (前端展示) ↓ 业务逻辑层 (WordPress插件) ↓ 数据访问层 (自定义数据库表+API) ↓ 数据存储层 (MySQL + 缓存) 第二部分:创建WordPress问卷调查插件 2.1 插件基础结构 首先创建插件主文件 wp-survey-dashboard.php: <?php /** * Plugin Name: WordPress问卷调查与数据看板 * Plugin URI: https://yourwebsite.com/ * Description: 集成在线问卷调查和实时数据看板的高级解决方案 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('WPSD_VERSION', '1.0.0'); define('WPSD_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('WPSD_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 class WP_Survey_Dashboard { private static $instance = null; public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } private function __construct() { $this->init_hooks(); } private function init_hooks() { // 激活/停用钩子 register_activation_hook(__FILE__, array($this, 'activate')); register_deactivation_hook(__FILE__, array($this, 'deactivate')); // 初始化 add_action('init', array($this, 'init')); // 管理菜单 add_action('admin_menu', array($this, 'add_admin_menu')); // 加载脚本和样式 add_action('wp_enqueue_scripts', array($this, 'enqueue_frontend_assets')); add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets')); } public function activate() { // 创建数据库表 $this->create_database_tables(); // 设置默认选项 update_option('wpsd_version', WPSD_VERSION); // 刷新重写规则 flush_rewrite_rules(); } public function deactivate() { // 清理临时数据 // 注意:不删除调查数据 flush_rewrite_rules(); } public function init() { // 注册自定义文章类型 $this->register_post_types(); // 注册短代码 add_shortcode('survey_form', array($this, 'survey_form_shortcode')); add_shortcode('survey_dashboard', array($this, 'survey_dashboard_shortcode')); // 初始化REST API add_action('rest_api_init', array($this, 'register_rest_routes')); } // 其他方法将在后续部分实现 } // 启动插件 WP_Survey_Dashboard::get_instance(); 2.2 创建数据库表结构 在 activate 方法中调用的 create_database_tables 方法: private function create_database_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); // 调查表 $table_surveys = $wpdb->prefix . 'wpsd_surveys'; $table_results = $wpdb->prefix . 'wpsd_results'; $table_answers = $wpdb->prefix . 'wpsd_answers'; // 创建调查表 $sql_surveys = "CREATE TABLE IF NOT EXISTS $table_surveys ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, title VARCHAR(255) NOT NULL, description TEXT, settings LONGTEXT, status VARCHAR(20) DEFAULT 'draft', created_by BIGINT(20) UNSIGNED, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY status (status), KEY created_by (created_by) ) $charset_collate;"; // 创建结果表 $sql_results = "CREATE TABLE IF NOT EXISTS $table_results ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, survey_id BIGINT(20) UNSIGNED NOT NULL, user_id BIGINT(20) UNSIGNED, user_ip VARCHAR(45), user_agent TEXT, session_id VARCHAR(100), completed TINYINT(1) DEFAULT 0, started_at DATETIME DEFAULT CURRENT_TIMESTAMP, completed_at DATETIME, metadata LONGTEXT, PRIMARY KEY (id), KEY survey_id (survey_id), KEY user_id (user_id), KEY session_id (session_id), KEY completed (completed) ) $charset_collate;"; // 创建答案表 $sql_answers = "CREATE TABLE IF NOT EXISTS $table_answers ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, result_id BIGINT(20) UNSIGNED NOT NULL, question_id VARCHAR(100) NOT NULL, question_type VARCHAR(50) NOT NULL, question_text TEXT, answer_value LONGTEXT, answer_text TEXT, answered_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY result_id (result_id), KEY question_id (question_id), KEY question_type (question_type) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql_surveys); dbDelta($sql_results); dbDelta($sql_answers); } 2.3 注册自定义文章类型和分类 扩展 init 方法中的 register_post_types: private function register_post_types() { // 注册调查自定义文章类型 register_post_type('wpsd_survey', array( 'labels' => array( 'name' => __('问卷调查', 'wp-survey-dashboard'), 'singular_name' => __('调查', 'wp-survey-dashboard'), 'add_new' => __('添加新调查', 'wp-survey-dashboard'), 'add_new_item' => __('添加新调查', 'wp-survey-dashboard'), 'edit_item' => __('编辑调查', 'wp-survey-dashboard'), 'new_item' => __('新调查', 'wp-survey-dashboard'), 'view_item' => __('查看调查', 'wp-survey-dashboard'), 'search_items' => __('搜索调查', 'wp-survey-dashboard'), 'not_found' => __('未找到调查', 'wp-survey-dashboard'), 'not_found_in_trash' => __('回收站中无调查', 'wp-survey-dashboard'), ), 'public' => true, 'has_archive' => true, 'rewrite' => array('slug' => 'surveys'), 'supports' => array('title', 'editor', 'author', 'revisions'), 'menu_icon' => 'dashicons-chart-bar', 'show_in_rest' => true, 'rest_base' => 'wpsd_surveys', 'capability_type' => 'post', 'capabilities' => array( 'create_posts' => 'create_wpsd_surveys', ), 'map_meta_cap' => true, ) ); // 注册调查分类 register_taxonomy('survey_category', 'wpsd_survey', array( 'labels' => array( 'name' => __('调查分类', 'wp-survey-dashboard'), 'singular_name' => __('分类', 'wp-survey-dashboard'), ), 'hierarchical' => true, 'show_in_rest' => true, 'show_admin_column' => true, ) ); } 第三部分:构建问卷调查前端功能 3.1 创建问卷调查表单短代码 实现 survey_form_shortcode 方法: public function survey_form_shortcode($atts) { // 提取短代码属性 $atts = shortcode_atts(array( 'id' => 0, 'title' => 'yes', 'description' => 'yes', ), $atts, 'survey_form'); $survey_id = intval($atts['id']); if (!$survey_id) { return '<div class="wpsd-error">请指定有效的调查ID</div>'; } // 获取调查数据 global $wpdb; $table_surveys = $wpdb->prefix . 'wpsd_surveys'; $survey = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_surveys WHERE id = %d AND status = 'published'", $survey_id )); if (!$survey) { return '<div class="wpsd-error">调查不存在或未发布</div>'; } // 解析调查设置 $settings = json_decode($survey->settings, true); $questions = isset($settings['questions']) ? $settings['questions'] : array(); // 生成唯一会话ID $session_id = $this->generate_session_id(); // 输出调查表单 ob_start(); ?> <div class="wpsd-survey-container" data-survey-id="<?php echo esc_attr($survey_id); ?>"> <div class="wpsd-survey-header"> <?php if ($atts['title'] === 'yes') : ?> <h2 class="wpsd-survey-title"><?php echo esc_html($survey->title); ?></h2> <?php endif; ?> <?php if ($atts['description'] === 'yes' && !empty($survey->description)) : ?> <div class="wpsd-survey-description"> <?php echo wp_kses_post($survey->description); ?> </div> <?php endif; ?> </div> <form id="wpsd-survey-form-<?php echo esc_attr($survey_id); ?>" class="wpsd-survey-form" method="post"> <input type="hidden" name="survey_id" value="<?php echo esc_attr($survey_id); ?>"> <input type="hidden" name="session_id" value="<?php echo esc_attr($session_id); ?>"> <input type="hidden" name="wpsd_nonce" value="<?php echo wp_create_nonce('wpsd_submit_survey'); ?>"> <div class="wpsd-questions-container"> <?php foreach ($questions as $index => $question) : ?> <div class="wpsd-question" data-question-id="<?php echo esc_attr($question['id']); ?>" data-question-type="<?php echo esc_attr($question['type']); ?>"> <div class="wpsd-question-header"> <h3 class="wpsd-question-title"> <span class="wpsd-question-number"><?php echo $index + 1; ?>.</span> <?php echo esc_html($question['title']); ?> <?php if (isset($question['required']) && $question['required']) : ?> <span class="wpsd-required">*</span> <?php endif; ?> </h3> <?php if (!empty($question['description'])) : ?> <div class="wpsd-question-description"> <?php echo esc_html($question['description']); ?> </div> <?php endif; ?> </div> <div class="wpsd-question-body"> <?php echo $this->render_question_field($question); ?> </div> </div> <?php endforeach; ?> </div> <div class="wpsd-form-footer"> <button type="submit" class="wpsd-submit-button"> <?php _e('提交调查', 'wp-survey-dashboard'); ?> </button> <div class="wpsd-form-message"></div> </div> </form> </div> <?php return ob_get_clean(); } private function render_question_field($question) { $field_html = ''; $question_id = esc_attr($question['id']); $field_name = "answers[$question_id]"; switch ($question['type']) { case 'radio': case 'checkbox': $field_html .= '<div class="wpsd-options-container">'; foreach ($question['options'] as $option) { $option_id = esc_attr($option['id']); $input_id = "{$question_id}_{$option_id}"; if ($question['type'] === 'radio') { $field_html .= '<div class="wpsd-option">'; $field_html .= '<input type="radio" id="' . $input_id . '" name="' . $field_name . '" value="' . esc_attr($option['value']) . '">'; $field_html .= '<label for="' . $input_id . '">' . esc_html($option['label']) . '</label>'; $field_html .= '</div>'; } else { $field_html .= '<div class="wpsd-option">'; $field_html .= '<input type="checkbox" id="' . $input_id . '" name="' . $field_name . '[]" value="' . esc_attr($option['value']) . '">'; $field_html .= '<label for="' . $input_id . '">' . esc_html($option['label']) . '</label>'; $field_html .= '</div>'; } } $field_html .= '</div>'; break; case 'select': $field_html .= '<select name="' . $field_name . '" class="wpsd-select">'; if (isset($question['placeholder'])) { $field_html .= '<option value="">' . esc_html($question['placeholder']) . '</option>'; } foreach ($question['options'] as $option) { $field_html .= '<option value="' . esc_attr($option['value']) . '">' . esc_html($option['label']) . '</option>'; } $field_html .= '</select>'; break; case 'rating': $field_html .= '<div class="wpsd-rating-container" data-max-rating="' . esc_attr($question['max_rating']) . '">'; for ($i = 1; $i <= $question['max_rating']; $i++) { $field_html .= '<div class="wpsd-rating-star" data-value="' . $i . '">'; $field_html .= '<span class="wpsd-star-icon">★</span>'; $field_html .= '</div>'; } $field_html .= '<input type="hidden" name="' . $field_name . '" value="">'; $field_html .= '</div>'; break; case 'text': case 'textarea': case 'email': $input_type = $question['type'] === 'textarea' ? 'textarea' : 'text'; $input_type_attr = $question['type'] === 'email' ? 'email' : 'text'; if ($input_type === 'textarea') { $field_html .= '<textarea name="' . $field_name . '" class="wpsd-textarea"'; if (isset($question['placeholder'])) { $field_html .= ' placeholder="' . esc_attr($question['placeholder']) . '"'; } if (isset($question['rows'])) { $field_html .= ' rows="' . esc_attr($question['rows']) . '"'; } $field_html .= '></textarea>'; } else { $field_html .= '<input type="' . $input_type_attr . '" name="' . $field_name . '" class="wpsd-input"'; if (isset($question['placeholder'])) { $field_html .= ' placeholder="' . esc_attr($question['placeholder']) . '"'; } if (isset($question['maxlength'])) { $field_html .= ' maxlength="' . esc_attr($question['maxlength']) . '"'; } $field_html .= '>'; } break; case 'matrix': $field_html .= '<div class="wpsd-matrix-container">'; $field_html .= '<table class="wpsd-matrix-table">'; $field_html .= '<thead><tr><th></th>'; foreach ($question['columns'] as $column) { $field_html .= '<th>' . esc_html($column['label']) . '</th>'; } $field_html .= '</tr></thead>'; $field_html .= '<tbody>'; foreach ($question['rows'] as $row) { $field_html .= '<tr>'; $field_html .= '<td class="wpsd-matrix-row-label">' . esc_html($row['label']) . '</td>'; foreach ($question['columns'] as $column) { $input_id = "{$question_id}_{$row['id']}_{$column['id']}"; $field_html .= '<td class="wpsd-matrix-cell">'; $field_html .= '<input type="radio" id="' . $input_id . '" name="answers[' . $question_id . '][' . $row['id'] . ']" value="' . esc_attr($column['value']) . '">'; $field_html .= '<label for="' . $input_id . '"></label>'; $field_html .= '</td>'; } $field_html .= '</tr>'; } $field_html .= '</tbody></table></div>'; break; default: $field_html .= '<p>不支持的问题类型: ' . esc_html($question['type']) . '</p>'; } return $field_html; } private function generate_session_id() { if (isset($_COOKIE['wpsd_session_id'])) { return sanitize_text_field($_COOKIE['wpsd_session_id']); } $session_id = wp_generate_uuid4(); setcookie('wpsd_session_id', $session_id, time() + 3600 * 24 * 7, COOKIEPATH, COOKIE_DOMAIN); return $session_id; } 3.2 前端脚本和样式 实现前端资源加载方法: public function enqueue_frontend_assets() { // 只在需要时加载 global $post; if (is_a($post, 'WP_Post') && (has_shortcode($post->post_content, 'survey_form') || has_shortcode($post->post_content, 'survey_dashboard'))) { // 加载Vue.js wp_enqueue_script('vue-js', 'https://cdn.jsdelivr.net/npm/vue@3.2.31/dist/vue.global.prod.js', array(), '3.2.31', true); // 加载Chart.js wp_enqueue_script('chart-js', 'https://cdn.jsdelivr.net/npm/chart.js@3.7.1/dist/chart.min.js', array(), '3.7.1', true); // 加载插件主脚本 wp_enqueue_script( 'wpsd-frontend', WPSD_PLUGIN_URL . 'assets/js/frontend.js', array('jquery', 'vue-js', 'chart-js'), WPSD_VERSION, true ); // 本地化脚本 wp_localize_script('wpsd-frontend', 'wpsd_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'rest_url' => rest_url('wpsd/v1/'), 'nonce' => wp_create_nonce('wpsd_ajax_nonce') )); // 加载样式 wp_enqueue_style( 'wpsd-frontend-style', WPSD_PLUGIN_URL . 'assets/css/frontend.css', array(), WPSD_VERSION ); } } 创建前端JavaScript文件 assets/js/frontend.js: (function($) { 'use strict'; // 问卷调查提交处理 $(document).on('submit', '.wpsd-survey-form', function(e) { e.preventDefault(); const $form = $(this); const $submitBtn = $form.find('.wpsd-submit-button'); const $message = $form.find('.wpsd-form-message'); const formData = new FormData(this); // 禁用提交按钮 $submitBtn.prop('disabled', true).text('提交中...'); $message.removeClass('wpsd-success wpsd-error').text(''); // 验证必填字段 const requiredFields = $form.find('.wpsd-required'); let isValid = true; requiredFields.each(function() { const $question = $(this).closest('.wpsd-question'); const questionType = $question.data('question-type'); const questionId = $question.data('question-id'); let isAnswered = false; if (questionType === 'radio' || questionType === 'select') { isAnswered = $form.find(`[name="answers[${questionId}]"]:checked`).length > 0 || $form.find(`[name="answers[${questionId}]"]`).val() !== ''; } else if (questionType === 'checkbox') { isAnswered = $form.find(`[name="answers[${questionId}][]"]:checked`).length > 0; } else if (questionType === 'matrix') { const rowCount = $question.find('.wpsd-matrix-row-label').length; const answeredRows = $question.find('input[type="radio"]:checked').length; isAnswered = answeredRows === rowCount; } else { isAnswered = $form.find(`[name="answers[${questionId}]"]`).val().trim() !== ''; } if (!isAnswered) { $question.addClass('wpsd-error'); isValid = false; } else { $question.removeClass('wpsd-error'); } }); if (!isValid) { $message.addClass('wpsd-error').text('请填写所有必填问题'); $submitBtn.prop('disabled', false).text('提交调查'); return; } // 发送AJAX请求 $.ajax({ url: wpsd_ajax.ajax_url, type: 'POST', data: formData, processData: false, contentType: false, success: function(response) { if (response.success) { $message.addClass('wpsd-success').text(response.data.message); $form[0].reset(); // 更新评分控件 $form.find('.wpsd-rating-container').each(function() { $(this).find('.wpsd-rating-star').removeClass('active'); $(this).find('input[type="hidden"]').val(''); }); // 显示感谢信息 setTimeout(function() { $form.html(` <div class="wpsd-thank-you"> <h3>感谢参与!</h3> <p>您的反馈对我们非常重要。</p> <p>您可以在调查结果页面查看实时统计数据。</p> </div> `); }, 2000); } else { $message.addClass('wpsd-error').text(response.data.message); $submitBtn.prop('disabled', false).text('提交调查'); } }, error: function() { $message.addClass('wpsd-error').text('提交失败,请稍后重试'); $submitBtn.prop('disabled', false).text('提交调查'); } }); }); // 评分控件交互 $(document).on('mouseenter', '.wpsd-rating-star', function() { const $container = $(this).closest('.wpsd-rating-container'); const value = $(this).data('value'); $container.find('.wpsd-rating-star').each(function() { if ($(this).data('value') <= value) { $(this).addClass('hover'); } else { $(this).removeClass('hover'); } }); }); $(document).on('mouseleave', '.wpsd-rating-container', function() { $(this).find('.wpsd-rating-star').removeClass('hover'); }); $(document).on('click', '.wpsd-rating-star', function() { const $container = $(this).closest('.wpsd-rating-container'); const value = $(this).data('value'); const $hiddenInput = $container.find('input[type="hidden"]'); $container.find('.wpsd-rating-star').removeClass('active hover'); $container.find('.wpsd-rating-star').each(function() { if ($(this).data('value') <= value) { $(this).addClass('active'); } }); $hiddenInput.val(value); }); // 数据看板Vue应用 if (typeof Vue !== 'undefined' && $('.wpsd-dashboard-container').length > 0) { const { createApp } = Vue; const DashboardApp = { data() { return { surveyId: 0, loading: true, error: null, surveyData: null, chartData: {}, filters: { dateRange: 'all', startDate: '', endDate: '', questionFilter: 'all' }, realTime: false, refreshInterval: null }; }, mounted() { const container = document.querySelector('.wpsd-dashboard-container'); this.surveyId = container.dataset.surveyId; this.realTime = container.dataset.realtime === 'true'; this.loadSurveyData(); if (this.realTime) { this.startRealTimeUpdates(); } }, methods: { async loadSurveyData() { this.loading = true; this.error = null; try { const response = await fetch( `${wpsd_ajax.rest_url}survey/${this.surveyId}/results?` + new URLSearchParams(this.filters) ); if (!response.ok) { throw new Error('获取数据失败'); } const data = await response.json(); this.surveyData = data; this.prepareChartData(); } catch (err) { this.error = err.message; console.error('加载调查数据失败:', err); } finally { this.loading = false; } }, prepareChartData() { if (!this.surveyData || !this.surveyData.questions) return; this.chartData = {}; this.surveyData.questions.forEach(question => { if (question.type === 'radio' || question.type === 'select' || question.type === 'checkbox') { this.chartData[question.id] = this.createPieChartData(question); } else if (question.type === 'rating') { this.chartData[question.id] = this.createBarChartData(question); } else if (question.type === 'matrix') { this.chartData[question.id] = this.createMatrixChartData(question); } }); // 渲染图表 this.$nextTick(() => { this.renderCharts(); }); }, createPieChartData(question) { const labels = []; const data = []; const backgroundColors = []; question.options.forEach((option, index) => { labels.push(option.label); data.push(option.count || 0); backgroundColors.push(this.getColor(index)); }); return { type: 'pie', data: { labels: labels, datasets: [{ data: data, backgroundColor: backgroundColors, borderWidth: 1 }] }, options: { responsive: true, plugins: { legend: { position: 'right' }, title: { display: true, text: question.title } } } }; }, createBarChartData(question) { const labels = []; const data = []; for (let i = 1; i <= question.max_rating; i++) { labels.push(`${i}星`); const count = question.ratings ? (question.ratings[i] || 0) : 0; data.push(count); } return { type: 'bar', data: { labels: labels, datasets: [{ label: '评分分布', data: data, backgroundColor: 'rgba(54, 162, 235, 0.5)', borderColor: 'rgba(54, 162, 235, 1)', borderWidth: 1 }] }, options: { responsive: true, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } }, plugins: { title: { display: true, text: question.title } } } }; }, createMatrixChartData(question) { const labels = question.rows.map(row => row.label); const datasets = []; question.columns.forEach((column, colIndex) => { const data = question.rows.map(row => { const cell = question.matrix_data?.find( d => d.row_id === row.id && d.column_id === column.id ); return cell ? cell.count : 0; }); datasets.push({ label: column.label, data: data, backgroundColor: this.getColor(colIndex, 0.5), borderColor: this.getColor(colIndex, 1), borderWidth: 1 }); }); return { type: 'bar', data: { labels: labels, datasets: datasets }, options: { responsive: true, scales: { x: { stacked: true }, y: { stacked: true, beginAtZero: true } }, plugins: { title: { display: true, text: question.title } } } }; }, renderCharts() { Object.keys(this.chartData).forEach(questionId => { const canvas = document.getElementById(`chart-${questionId}`); if (!canvas) return; const ctx = canvas.getContext('2d'); const chartConfig = this.chartData[questionId]; // 销毁现有图表实例 if (canvas.chart) { canvas.chart.destroy(); } // 创建新图表 canvas.chart = new Chart(ctx, chartConfig); }); }, getColor(index, alpha = 1) { const colors = [ `rgba(255, 99, 132, ${alpha})`, `rgba(54, 162, 235, ${alpha})`, `rgba(255, 206, 86, ${alpha})`, `rgba(75, 192, 192, ${alpha})`, `rgba(153, 102, 255, ${alpha})`, `rgba(255, 159, 64, ${alpha})`, `rgba(199, 199, 199, ${alpha})`, `rgba(83, 102, 255, ${alpha})`, `rgba(40, 159, 64, ${alpha})`, `rgba(210, 199, 199, ${alpha})` ]; return colors[index % colors.length]; }, applyFilters() { this.loadSurveyData(); }, resetFilters() { this.filters = { dateRange: 'all', startDate: '', endDate: '', questionFilter: 'all' }; this.loadSurveyData(); }, startRealTimeUpdates() { // 每30秒更新一次数据 this.refreshInterval = setInterval(() => { this.loadSurveyData(); }, 30000); }, stopRealTimeUpdates() { if (this.refreshInterval) { clearInterval(this.refreshInterval); this.refreshInterval = null; } }, exportData(format) { const url = `${wpsd_ajax.rest_url}survey/${this.surveyId}/export?format=${format}`; window.open(url, '_blank'); } }, beforeUnmount() { this.stopRealTimeUpdates(); }, template: ` <div class="wpsd-dashboard"> <div v-if="loading" class="wpsd-loading"> 加载中... </div> <div v-else-if="error" class="wpsd-error"> {{ error }} </div> <div v-else> <!-- 控制面板 --> <div class="wpsd-dashboard-controls"> <div class="wpsd-filters"> <div class="wpsd-filter-group"> <label>时间范围:</label> <select v-model="filters.dateRange" @change="applyFilters"> <option value="all">全部时间</option> <option value="today">今天</option> <option value="yesterday">昨天</option> <option value="week">本周</option> <option value="month">本月</option> <option value="custom">自定义</option> </select> </div> <div v-if="filters.dateRange === 'custom'" class="wpsd-filter-group">

发表评论

一步步教你,在WordPress中添加网站内容词云与关键词分析工具

一步步教你,在WordPress中添加网站内容词云与关键词分析工具 引言:为什么WordPress网站需要内容分析工具 在当今信息爆炸的时代,网站内容管理已不再是简单的发布与展示。对于WordPress网站管理员和内容创作者而言,深入了解网站内容结构、关键词分布和用户关注点变得至关重要。词云和关键词分析工具能够直观展示网站内容的核心主题,帮助优化内容策略,提升SEO效果,并增强用户体验。 传统的WordPress功能虽然强大,但在内容分析方面往往需要依赖第三方插件,这些插件可能存在兼容性问题、性能负担或功能限制。通过代码二次开发实现自定义的词云与关键词分析工具,不仅能完全控制功能特性,还能确保与网站主题完美融合,提升整体性能。 本文将详细指导您如何通过WordPress代码二次开发,为您的网站添加专业级的内容词云与关键词分析功能。无论您是WordPress开发者还是有一定技术基础的管理员,都能跟随本文步骤实现这一实用功能。 第一部分:准备工作与环境搭建 1.1 开发环境要求 在开始开发之前,请确保您的环境满足以下要求: WordPress 5.0及以上版本 PHP 7.2及以上版本(推荐PHP 7.4+) 基本的HTML、CSS、JavaScript知识 对WordPress主题结构和插件开发有基本了解 代码编辑器(如VS Code、Sublime Text等) 本地或测试服务器环境 1.2 创建开发环境 为了避免影响生产网站,建议在本地或测试服务器上进行开发: 本地开发环境设置: 使用Local by Flywheel、XAMPP或MAMP搭建本地WordPress环境 安装一个干净的WordPress实例 选择并激活一个基础主题(如Twenty Twenty-One) 创建子主题:如果您计划修改现有主题,强烈建议创建子主题: /* Theme Name: My Custom Theme Child Template: twentytwentyone */ 将上述代码保存为style.css,放在新创建的子主题文件夹中。 启用调试模式:在wp-config.php中添加以下代码,以便在开发过程中查看错误信息: define('WP_DEBUG', true); define('WP_DEBUG_LOG', true); define('WP_DEBUG_DISPLAY', false); 1.3 工具与库准备 我们将使用以下开源库来构建词云和关键词分析功能: WordCloud.js:一个基于HTML5 Canvas的JavaScript词云库 Chart.js:用于创建关键词分析图表 自然语言处理工具:我们将使用PHP的文本处理功能,对于更高级的分析,可以考虑集成PHP-ML库 下载这些库文件,或通过CDN链接在项目中引用。 第二部分:词云功能设计与实现 2.1 词云功能设计思路 词云功能的核心是从网站内容中提取关键词,并根据词频生成可视化云图。我们的设计将包括以下组件: 数据收集模块:从文章、页面等内容中提取文本 文本处理模块:清洗文本,去除停用词,提取关键词 词频统计模块:计算每个关键词的出现频率 可视化模块:将词频数据转换为可视化词云 显示控制模块:提供配置选项,控制词云的显示方式 2.2 创建词云插件基础结构 我们将创建一个独立的WordPress插件来实现词云功能: 创建插件文件夹和主文件:在wp-content/plugins目录下创建"wordpress-content-analyzer"文件夹,并在其中创建主插件文件: <?php /** * Plugin Name: WordPress内容分析器 * Plugin URI: https://yourwebsite.com/ * Description: 为WordPress网站添加词云和关键词分析功能 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('WCA_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('WCA_PLUGIN_URL', plugin_dir_url(__FILE__)); define('WCA_VERSION', '1.0.0'); // 初始化插件 function wca_initialize_plugin() { // 检查WordPress版本 if (version_compare(get_bloginfo('version'), '5.0', '<')) { deactivate_plugins(basename(__FILE__)); wp_die(__('本插件需要WordPress 5.0或更高版本。', 'wordpress-content-analyzer')); } // 加载必要文件 require_once WCA_PLUGIN_DIR . 'includes/class-text-processor.php'; require_once WCA_PLUGIN_DIR . 'includes/class-word-cloud.php'; require_once WCA_PLUGIN_DIR . 'includes/class-keyword-analyzer.php'; } add_action('plugins_loaded', 'wca_initialize_plugin'); 2.3 文本处理与关键词提取 创建文本处理类,用于从WordPress内容中提取和清洗关键词: <?php // includes/class-text-processor.php class WCA_Text_Processor { private $stop_words; public function __construct() { // 中文停用词列表(简化版,实际应用中需要更完整的列表) $this->stop_words = array( '的', '了', '在', '是', '我', '有', '和', '就', '不', '人', '都', '一', '一个', '上', '也', '很', '到', '说', '要', '去', '你', '会', '着', '没有', '看', '好', '自己', '这', '那', '他', '她', '它' ); // 可以添加英文停用词 $english_stop_words = array( 'a', 'an', 'the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'having', 'do', 'does', 'did', 'doing' ); $this->stop_words = array_merge($this->stop_words, $english_stop_words); } /** * 从文章内容中提取文本 */ public function extract_text_from_post($post_id) { $post = get_post($post_id); if (!$post) { return ''; } // 提取标题和内容 $text = $post->post_title . ' ' . $post->post_content; // 移除HTML标签 $text = wp_strip_all_tags($text); // 移除短代码 $text = strip_shortcodes($text); return $text; } /** * 从多个文章中提取文本 */ public function extract_text_from_posts($post_ids = array()) { $all_text = ''; if (empty($post_ids)) { // 获取所有已发布的文章 $args = array( 'post_type' => 'post', 'post_status' => 'publish', 'posts_per_page' => -1, 'fields' => 'ids' ); $post_ids = get_posts($args); } foreach ($post_ids as $post_id) { $all_text .= $this->extract_text_from_post($post_id) . ' '; } return $all_text; } /** * 清洗文本并提取关键词 */ public function process_text($text, $max_words = 100) { // 转换为小写(针对英文) $text = mb_strtolower($text, 'UTF-8'); // 移除标点符号和数字 $text = preg_replace('/[[:punct:]]/u', ' ', $text); $text = preg_replace('/[0-9]+/', ' ', $text); // 分割为单词/词语 // 这里使用简单空格分割,中文需要更复杂的分词处理 $words = preg_split('/s+/', $text); // 移除空值和停用词 $words = array_filter($words, function($word) { return !empty($word) && !in_array($word, $this->stop_words) && mb_strlen($word, 'UTF-8') > 1; }); // 统计词频 $word_counts = array_count_values($words); // 按词频排序 arsort($word_counts); // 限制返回的词数 $word_counts = array_slice($word_counts, 0, $max_words, true); return $word_counts; } /** * 改进的中文分词方法(简单实现) */ public function chinese_segmentation($text) { // 这是一个简化的中文分词方法 // 实际应用中建议使用更专业的分词库,如jieba-php // 将文本按常见分隔符分割 $delimiters = array(',', '。', '!', '?', ';', '、', ' ', ',', '.', '!', '?', ';'); $text = str_replace($delimiters, ' ', $text); // 简单按字符分割(对于中文,这只是一个基础实现) // 更好的方法是使用字典匹配或机器学习分词 $words = array(); $length = mb_strlen($text, 'UTF-8'); for ($i = 0; $i < $length; $i++) { // 获取单个字符 $char = mb_substr($text, $i, 1, 'UTF-8'); // 如果是中文,尝试获取2-4个字符的词语 if (preg_match('/[x{4e00}-x{9fa5}]/u', $char)) { // 添加单字 $words[] = $char; // 尝试添加双字词 if ($i + 1 < $length) { $two_char = mb_substr($text, $i, 2, 'UTF-8'); if (preg_match('/^[x{4e00}-x{9fa5}]{2}$/u', $two_char)) { $words[] = $two_char; } } // 尝试添加三字词 if ($i + 2 < $length) { $three_char = mb_substr($text, $i, 3, 'UTF-8'); if (preg_match('/^[x{4e00}-x{9fa5}]{3}$/u', $three_char)) { $words[] = $three_char; } } } else { // 非中文字符,按空格分割的单词处理 if ($i > 0 && mb_substr($text, $i-1, 1, 'UTF-8') !== ' ') { continue; } // 提取英文单词 $j = $i; while ($j < $length && preg_match('/[a-z]/i', mb_substr($text, $j, 1, 'UTF-8'))) { $j++; } if ($j > $i) { $word = mb_substr($text, $i, $j-$i, 'UTF-8'); $words[] = $word; $i = $j - 1; } } } return $words; } } 2.4 词云可视化实现 创建词云类,用于生成词云数据并渲染可视化: <?php // includes/class-word-cloud.php class WCA_Word_Cloud { private $text_processor; public function __construct() { $this->text_processor = new WCA_Text_Processor(); // 注册短代码 add_shortcode('wordcloud', array($this, 'wordcloud_shortcode')); // 注册小工具 add_action('widgets_init', array($this, 'register_widget')); // 添加管理页面 add_action('admin_menu', array($this, 'add_admin_menu')); } /** * 生成词云数据 */ public function generate_wordcloud_data($post_ids = array(), $max_words = 50) { // 提取文本 $text = $this->text_processor->extract_text_from_posts($post_ids); // 处理文本并获取词频 $word_counts = $this->text_processor->process_text($text, $max_words); // 格式化数据供JavaScript使用 $wordcloud_data = array(); foreach ($word_counts as $word => $count) { $wordcloud_data[] = array( 'text' => $word, 'size' => $count * 10, // 根据词频调整大小 'count' => $count ); } return $wordcloud_data; } /** * 渲染词云HTML */ public function render_wordcloud($post_ids = array(), $max_words = 50, $width = 800, $height = 600) { // 获取词云数据 $wordcloud_data = $this->generate_wordcloud_data($post_ids, $max_words); // 生成唯一ID $cloud_id = 'wordcloud-' . uniqid(); // 输出HTML结构 $output = '<div class="wordcloud-container">'; $output .= '<div id="' . $cloud_id . '" class="wordcloud-canvas" style="width: ' . $width . 'px; height: ' . $height . 'px;"></div>'; $output .= '<div class="wordcloud-legend"></div>'; $output .= '</div>'; // 添加JavaScript代码 $output .= '<script type="text/javascript">'; $output .= 'jQuery(document).ready(function($) {'; $output .= 'var wordcloudData = ' . json_encode($wordcloud_data) . ';'; $output .= 'WCA_WordCloud.render("#' . $cloud_id . '", wordcloudData);'; $output .= '});'; $output .= '</script>'; return $output; } /** * 短代码处理函数 */ public function wordcloud_shortcode($atts) { // 解析短代码属性 $atts = shortcode_atts(array( 'max_words' => 50, 'width' => 800, 'height' => 600, 'post_ids' => '' ), $atts, 'wordcloud'); // 处理post_ids参数 $post_ids = array(); if (!empty($atts['post_ids'])) { $post_ids = array_map('intval', explode(',', $atts['post_ids'])); } // 渲染词云 return $this->render_wordcloud( $post_ids, intval($atts['max_words']), intval($atts['width']), intval($atts['height']) ); } /** * 添加管理菜单 */ public function add_admin_menu() { add_options_page( '词云设置', '内容词云', 'manage_options', 'wordcloud-settings', array($this, 'render_admin_page') ); } /** * 渲染管理页面 */ public function render_admin_page() { ?> <div class="wrap"> <h1>词云设置</h1> <form method="post" action="options.php"> <?php settings_fields('wordcloud_settings'); do_settings_sections('wordcloud_settings'); ?> <table class="form-table"> <tr> <th scope="row">默认显示词数</th> <td> <input type="number" name="wordcloud_max_words" value="<?php echo esc_attr(get_option('wordcloud_max_words', 50)); ?>" min="10" max="200"> <p class="description">词云中默认显示的关键词数量</p> </td> </tr> <tr> <th scope="row">排除词语</th> <td> <textarea name="wordcloud_exclude_words" rows="5" cols="50"><?php echo esc_textarea(get_option('wordcloud_exclude_words', '')); ?></textarea> <p class="description">每行一个,这些词语不会出现在词云中</p> </td> </tr> </table> <?php submit_button(); ?> </form> <h2>预览</h2> <div id="wordcloud-preview"> <?php echo $this->render_wordcloud(); ?> </div> </div> <?php } /** * 注册小工具 */ public function register_widget() { register_widget('WCA_WordCloud_Widget'); } } // 词云小工具类 class WCA_WordCloud_Widget extends WP_Widget { public function __construct() { parent::__construct( 'wca_wordcloud_widget', '内容词云', array('description' => '显示网站内容词云') ); } public function widget($args, $instance) { echo $args['before_widget']; if (!empty($instance['title'])) { echo $args['before_title'] . apply_filters('widget_title', $instance['title']) . $args['after_title']; } $max_words = !empty($instance['max_words']) ? $instance['max_words'] : 30; instance['width'] : 300; $height = !empty($instance['height']) ? $instance['height'] : 300; $wordcloud = new WCA_Word_Cloud(); echo $wordcloud->render_wordcloud(array(), $max_words, $width, $height); echo $args['after_widget']; } public function form($instance) { $title = !empty($instance['title']) ? $instance['title'] : '内容词云'; $max_words = !empty($instance['max_words']) ? $instance['max_words'] : 30; $width = !empty($instance['width']) ? $instance['width'] : 300; $height = !empty($instance['height']) ? $instance['height'] : 300; ?> <p> <label for="<?php echo esc_attr($this->get_field_id('title')); ?>">标题:</label> <input class="widefat" id="<?php echo esc_attr($this->get_field_id('title')); ?>" name="<?php echo esc_attr($this->get_field_name('title')); ?>" type="text" value="<?php echo esc_attr($title); ?>"> </p> <p> <label for="<?php echo esc_attr($this->get_field_id('max_words')); ?>">显示词数:</label> <input class="widefat" id="<?php echo esc_attr($this->get_field_id('max_words')); ?>" name="<?php echo esc_attr($this->get_field_name('max_words')); ?>" type="number" value="<?php echo esc_attr($max_words); ?>" min="10" max="100"> </p> <p> <label for="<?php echo esc_attr($this->get_field_id('width')); ?>">宽度:</label> <input class="widefat" id="<?php echo esc_attr($this->get_field_id('width')); ?>" name="<?php echo esc_attr($this->get_field_name('width')); ?>" type="number" value="<?php echo esc_attr($width); ?>" min="200" max="1000"> </p> <p> <label for="<?php echo esc_attr($this->get_field_id('height')); ?>">高度:</label> <input class="widefat" id="<?php echo esc_attr($this->get_field_id('height')); ?>" name="<?php echo esc_attr($this->get_field_name('height')); ?>" type="number" value="<?php echo esc_attr($height); ?>" min="200" max="1000"> </p> <?php } public function update($new_instance, $old_instance) { $instance = array(); $instance['title'] = (!empty($new_instance['title'])) ? strip_tags($new_instance['title']) : ''; $instance['max_words'] = (!empty($new_instance['max_words'])) ? intval($new_instance['max_words']) : 30; $instance['width'] = (!empty($new_instance['width'])) ? intval($new_instance['width']) : 300; $instance['height'] = (!empty($new_instance['height'])) ? intval($new_instance['height']) : 300; return $instance; } } ### 2.5 前端JavaScript实现 创建前端JavaScript文件,用于渲染词云可视化: // assets/js/wordcloud-renderer.js var WCA_WordCloud = (function() { // 私有变量 var defaultColors = [ '#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf' ]; // 渲染词云 function render(containerSelector, wordData) { var container = document.querySelector(containerSelector); if (!container) { console.error('词云容器未找到: ' + containerSelector); return; } // 清空容器 container.innerHTML = ''; // 计算最大和最小词频 var maxSize = Math.max.apply(Math, wordData.map(function(item) { return item.size; })); var minSize = Math.min.apply(Math, wordData.map(function(item) { return item.size; })); // 创建词云元素 wordData.forEach(function(wordObj, index) { var wordElement = document.createElement('span'); wordElement.className = 'cloud-word'; wordElement.textContent = wordObj.text; // 计算字体大小(基于词频) var fontSize = 14 + (wordObj.size - minSize) / (maxSize - minSize) * 36; wordElement.style.fontSize = fontSize + 'px'; // 设置随机颜色 var colorIndex = index % defaultColors.length; wordElement.style.color = defaultColors[colorIndex]; // 设置透明度 var opacity = 0.7 + (wordObj.size - minSize) / (maxSize - minSize) * 0.3; wordElement.style.opacity = opacity; // 设置鼠标悬停效果 wordElement.style.cursor = 'pointer'; wordElement.style.display = 'inline-block'; wordElement.style.margin = '5px'; wordElement.style.padding = '2px 5px'; wordElement.style.transition = 'all 0.3s ease'; wordElement.addEventListener('mouseover', function() { this.style.transform = 'scale(1.2)'; this.style.zIndex = '100'; this.style.backgroundColor = 'rgba(0,0,0,0.1)'; this.style.borderRadius = '3px'; }); wordElement.addEventListener('mouseout', function() { this.style.transform = 'scale(1)'; this.style.zIndex = '1'; this.style.backgroundColor = 'transparent'; }); // 点击事件:显示词频 wordElement.addEventListener('click', function() { alert('关键词: ' + wordObj.text + 'n出现次数: ' + wordObj.count); }); container.appendChild(wordElement); }); // 添加CSS样式 addCloudStyles(); } // 添加词云样式 function addCloudStyles() { if (document.getElementById('wordcloud-styles')) { return; } var styleElement = document.createElement('style'); styleElement.id = 'wordcloud-styles'; styleElement.textContent = ` .wordcloud-container { text-align: center; padding: 20px; background: #f9f9f9; border-radius: 8px; margin: 20px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } .wordcloud-canvas { margin: 0 auto; line-height: 1.5; position: relative; overflow: hidden; } .cloud-word { display: inline-block; margin: 5px; padding: 2px 5px; transition: all 0.3s ease; font-family: 'Microsoft YaHei', sans-serif; } .cloud-word:hover { transform: scale(1.2); z-index: 100; } .wordcloud-legend { margin-top: 20px; font-size: 12px; color: #666; } `; document.head.appendChild(styleElement); } // 公共API return { render: render, updateColors: function(colors) { defaultColors = colors; } }; })(); ## 第三部分:关键词分析工具实现 ### 3.1 关键词分析功能设计 关键词分析工具将提供以下功能: 1. **词频统计**:显示关键词出现频率 2. **趋势分析**:展示关键词随时间的变化 3. **相关性分析**:分析关键词之间的关联 4. **SEO建议**:基于关键词分析提供优化建议 ### 3.2 创建关键词分析类 <?php// includes/class-keyword-analyzer.php class WCA_Keyword_Analyzer { private $text_processor; public function __construct() { $this->text_processor = new WCA_Text_Processor(); // 注册短代码 add_shortcode('keyword_analysis', array($this, 'keyword_analysis_shortcode')); // 添加REST API端点 add_action('rest_api_init', array($this, 'register_rest_routes')); } /** * 获取关键词统计数据 */ public function get_keyword_stats($time_range = 'all', $limit = 20) { global $wpdb; // 根据时间范围确定日期条件 $date_condition = ''; if ($time_range !== 'all') { $date = date('Y-m-d', strtotime('-' . $time_range)); $date_condition = " AND post_date >= '{$date}'"; } // 获取文章内容 $query = " SELECT ID, post_title, post_content, post_date FROM {$wpdb->posts} WHERE post_type = 'post' AND post_status = 'publish' {$date_condition} ORDER BY post_date DESC "; $posts = $wpdb->get_results($query); // 提取所有文本 $all_text = ''; $posts_by_month = array(); foreach ($posts as $post) { $text = $post->post_title . ' ' . $post->post_content; $text = wp_strip_all_tags($text); $text = strip_shortcodes($text); $all_text .= $text . ' '; // 按月份分组 $month = date('Y-m', strtotime($post->post_date)); if (!isset($posts_by_month[$month])) { $posts_by_month[$month] = ''; } $posts_by_month[$month] .= $text . ' '; } // 处理文本获取关键词 $all_keywords = $this->text_processor->process_text($all_text, $limit * 2); // 获取每个关键词的月度趋势 $monthly_trends = array(); $top_keywords = array_slice($all_keywords, 0, $limit, true); foreach (array_keys($top_keywords) as $keyword) { $monthly_trends[$keyword] = array(); foreach ($posts_by_month as $month => $month_text) { // 统计该关键词在当月出现的次数 $month_word_counts = $this->text_processor->process_text($month_text, 100); $monthly_trends[$keyword][$month] = isset($month_word_counts[$keyword]) ? $month_word_counts[$keyword] : 0; } } // 计算关键词相关性(简化版) $correlations = $this->calculate_correlations($top_keywords, $posts); return array( 'top_keywords' => $top_keywords, 'monthly_trends' => $monthly_trends, 'correlations' => $correlations, 'total_posts' => count($posts), 'time_range' => $time_range ); } /** * 计算关键词相关性 */ private function calculate_correlations($keywords, $posts) { $correlations = array(); $keyword_list = array_keys($keywords); // 初始化相关性矩阵 foreach ($keyword_list as $keyword1) { foreach ($keyword_list as $keyword2) { if ($keyword1 !== $keyword2) { $correlations[$keyword1][$keyword2] = 0; } } } // 统计共同出现次数 foreach ($posts as $post) { $text = $post->post_title . ' ' . $post->post_content; $text = wp_strip_all_tags($text); $text = mb_strtolower($text, 'UTF-8'); foreach ($keyword_list as $keyword1) { if (strpos($text, $keyword1) !== false) { foreach ($keyword_list as $keyword2) { if ($keyword1 !== $keyword2 && strpos($text, $keyword2) !== false) { $correlations[$keyword1][$keyword2]++; } } } } } return $correlations; } /** * 生成SEO建议 */ public function generate_seo_recommendations($keyword_stats) { $recommendations = array(); $top_keywords = $keyword_stats['top_keywords']; // 分析关键词密度 $total_words = array_sum($top_keywords); foreach ($top_keywords as $keyword => $count) { $density = ($count / $total_words) * 100; if ($density < 1) { $recommendations[] = sprintf( '关键词"%s"的密度较低(%.2f%%),建议在相关文章中增加使用频率', $keyword, $density ); } elseif ($density > 5) { $recommendations[] = sprintf( '关键词"%s"的密度较高(%.2f%%),注意避免关键词堆砌', $keyword, $density ); } } // 检查长尾关键词 if (count($top_keywords) < 10) { $recommendations[] = '网站关键词数量较少,建议增加内容多样性,覆盖更多长尾关键词'; } // 基于相关性建议 $correlations = $keyword_stats['correlations']; $high_correlations = array(); foreach ($correlations as $kw1 => $related) { foreach ($related as $kw2 => $score) { if ($score > 5 && !isset($high_correlations[$kw2 . '-' . $kw1])) { $high_correlations[$kw1 . '-' . $kw2] = array( 'keywords' => array($kw1, $kw2), 'score' => $score ); } } } if (!empty($high_correlations)) { $recommendations[] = '以下关键词经常同时出现,可以考虑创建相关内容:'; foreach ($high_correlations as $pair) { $recommendations[] = sprintf( '- "%s" 和 "%s" (共同出现%d次)', $pair['keywords'][0], $pair['keywords'][1], $pair['score'] ); } } return $recommendations; } /** * 渲染关键词分析报告 */ public function render_analysis_report($time_range = 'all', $limit = 15) { // 获取统计数据 $stats = $this->get_keyword_stats($time_range, $limit); $recommendations = $this->generate_seo_recommendations($stats); // 生成唯一ID $report_id = 'keyword-analysis-' . uniqid(); // 输出HTML结构 $output = '<div class="keyword-analysis-report" id="' . $report_id . '">'; $output .= '<div class="report-header">'; $output .= '<h3>关键词分析报告</h3>'; $output .= '<p>分析时间范围: ' . $this->get_time_range_label($time_range) . ' | 分析文章: ' . $stats['total_posts'] . '篇</p>'; $output .= '</div>'; // 关键词排名表格 $output .= '<div class="keyword-ranking">'; $output .= '<h4>关键词排名TOP' . $limit . '</h4>'; $output .= '<table class="wp-list-table widefat fixed striped">'; $output .= '<thead><tr><th>排名</th><th>关键词</th><th>出现次数</th><th>占比</th><th>趋势</th></tr></thead>'; $output .= '<tbody>'; $total = array_sum($stats['top_keywords']); $rank = 1; foreach ($stats['top_keywords'] as $keyword => $count) { $percentage = round(($count / $total) * 100, 2); $trend = $this->get_trend_icon($stats['monthly_trends'][$keyword]); $output .= '<tr>'; $output .= '<td>' . $rank . '</td>'; $output .= '<td><strong>' . esc_html($keyword) . '</strong></td>'; $output .= '<td>' . $count . '</td>'; $output .= '<td>' . $percentage . '%</td>'; $output .= '<td>' . $trend . '</td>'; $output .= '</tr>'; $rank++; } $output .= '</tbody></table></div>'; // 趋势图表容器 $output .= '<div class="trend-chart-container">'; $output .= '<h4>关键词趋势分析</h4>'; $output .= '<div class="chart-wrapper">'; $output .= '<canvas id="trend-chart-' . $report_id . '" width="800" height="400"></canvas>'; $output .= '</div>'; $output .= '</div>'; // SEO建议 $output .= '<div class="seo-recommendations">'; $output .= '<h4>SEO优化建议</h4>'; $output .= '<ul>'; foreach ($recommendations as $recommendation) { $output .= '<li>' . esc_html($recommendation) . '</li>'; } $output .= '</ul>'; $output .= '</div>'; $output .= '</div>'; // 添加JavaScript代码 $output .= $this->get

发表评论

实战教程,为网站集成智能化的多平台账号一键登录与授权管理

实战教程:为网站集成智能化的多平台账号一键登录与授权管理 引言:智能化登录体验的重要性 在当今互联网环境中,用户拥有多个平台的账号已成为常态。从社交媒体到专业工具,从娱乐应用到工作软件,每个用户平均管理着超过7个不同平台的账号。这种碎片化的账号体系给用户带来了记忆负担和安全风险,同时也为网站运营者带来了用户转化率低、注册流程复杂等挑战。 智能化多平台账号一键登录与授权管理正是解决这一痛点的关键技术。通过集成主流社交平台和第三方服务的登录接口,网站可以为用户提供无缝的登录体验,同时获取用户授权的基本信息,实现个性化服务。本教程将深入探讨如何通过WordPress程序的代码二次开发,实现这一功能,并扩展常用互联网小工具功能。 第一章:理解多平台登录的技术原理 1.1 OAuth 2.0协议基础 OAuth 2.0是目前最流行的授权框架,它允许用户授权第三方应用访问他们在其他服务提供者处的信息,而无需将用户名和密码提供给第三方应用。OAuth 2.0的核心流程包括: 授权请求:用户被重定向到服务提供者的授权页面 用户授权:用户在授权页面同意授权请求 授权码返回:服务提供者将授权码返回给第三方应用 令牌交换:第三方应用使用授权码交换访问令牌 资源访问:第三方应用使用访问令牌访问受保护的资源 1.2 OpenID Connect扩展 OpenID Connect是建立在OAuth 2.0之上的身份层,它添加了身份验证功能,使客户端能够验证用户的身份并获取基本的用户信息。与纯OAuth 2.0相比,OpenID Connect提供了标准的身份信息(ID Token)和用户信息端点。 1.3 主流平台登录接口对比 平台 协议支持 特点 用户基数 微信登录 OAuth 2.0 中国用户覆盖广,需企业资质 12亿+ QQ登录 OAuth 2.0 年轻用户群体多 5.7亿+ 微博登录 OAuth 2.0 媒体属性强,信息传播快 5.5亿+ GitHub登录 OAuth 2.0 开发者群体,技术社区 7300万+ Google登录 OAuth 2.0 + OpenID 国际用户,技术成熟 20亿+ Facebook登录 OAuth 2.0 国际社交网络覆盖广 29亿+ 第二章:WordPress开发环境准备 2.1 本地开发环境搭建 要实现WordPress的代码二次开发,首先需要搭建合适的开发环境: 服务器环境:推荐使用XAMPP、MAMP或Local by Flywheel WordPress安装:下载最新版WordPress并完成基础安装 代码编辑器:推荐VS Code、PHPStorm或Sublime Text 调试工具:安装Query Monitor、Debug Bar等调试插件 版本控制:初始化Git仓库,建立开发分支 2.2 创建自定义插件框架 为了避免主题更新导致功能丢失,我们将通过创建独立插件的方式实现功能: <?php /** * Plugin Name: 智能多平台登录与工具集成 * Plugin URI: https://yourwebsite.com/ * Description: 为WordPress网站集成多平台一键登录与常用工具功能 * Version: 1.0.0 * Author: Your Name * License: GPL v2 or later */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('SMPL_VERSION', '1.0.0'); define('SMPL_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('SMPL_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 require_once SMPL_PLUGIN_DIR . 'includes/class-core.php'; function smpl_init() { return SMPL_Core::get_instance(); } add_action('plugins_loaded', 'smpl_init'); 2.3 数据库表设计 为存储用户授权信息和登录记录,我们需要创建自定义数据库表: // 在插件激活时创建表 register_activation_hook(__FILE__, 'smpl_create_tables'); function smpl_create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'smpl_user_connections'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, platform varchar(50) NOT NULL, platform_user_id varchar(100) NOT NULL, access_token text, refresh_token text, token_expiry datetime, user_data text, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY platform_user (platform, platform_user_id), KEY user_id (user_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 创建登录日志表 $log_table = $wpdb->prefix . 'smpl_login_logs'; $log_sql = "CREATE TABLE IF NOT EXISTS $log_table ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20), platform varchar(50), ip_address varchar(45), user_agent text, status varchar(20), created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY user_id (user_id), KEY platform (platform) ) $charset_collate;"; dbDelta($log_sql); } 第三章:多平台登录接口集成 3.1 平台应用注册与配置管理 在集成各平台登录功能前,需要先在各平台开发者中心注册应用,获取必要的API密钥: class SMPL_Platform_Config { private $platforms = []; public function __construct() { $this->init_platforms(); } private function init_platforms() { $this->platforms = [ 'wechat' => [ 'name' => '微信', 'auth_url' => 'https://open.weixin.qq.com/connect/qrconnect', 'token_url' => 'https://api.weixin.qq.com/sns/oauth2/access_token', 'userinfo_url' => 'https://api.weixin.qq.com/sns/userinfo', 'app_id' => get_option('smpl_wechat_appid', ''), 'app_secret' => get_option('smpl_wechat_secret', ''), 'scope' => 'snsapi_login', 'enabled' => get_option('smpl_wechat_enabled', false) ], 'qq' => [ 'name' => 'QQ', 'auth_url' => 'https://graph.qq.com/oauth2.0/authorize', 'token_url' => 'https://graph.qq.com/oauth2.0/token', 'userinfo_url' => 'https://graph.qq.com/user/get_user_info', 'app_id' => get_option('smpl_qq_appid', ''), 'app_secret' => get_option('smpl_qq_secret', ''), 'scope' => 'get_user_info', 'enabled' => get_option('smpl_qq_enabled', false) ], 'weibo' => [ 'name' => '微博', 'auth_url' => 'https://api.weibo.com/oauth2/authorize', 'token_url' => 'https://api.weibo.com/oauth2/access_token', 'userinfo_url' => 'https://api.weibo.com/2/users/show.json', 'app_id' => get_option('smpl_weibo_appkey', ''), 'app_secret' => get_option('smpl_weibo_secret', ''), 'scope' => '', 'enabled' => get_option('smpl_weibo_enabled', false) ], 'github' => [ 'name' => 'GitHub', 'auth_url' => 'https://github.com/login/oauth/authorize', 'token_url' => 'https://github.com/login/oauth/access_token', 'userinfo_url' => 'https://api.github.com/user', 'app_id' => get_option('smpl_github_client_id', ''), 'app_secret' => get_option('smpl_github_secret', ''), 'scope' => 'user', 'enabled' => get_option('smpl_github_enabled', false) ] ]; } public function get_platform_config($platform) { return isset($this->platforms[$platform]) ? $this->platforms[$platform] : false; } public function get_enabled_platforms() { return array_filter($this->platforms, function($platform) { return $platform['enabled'] && !empty($platform['app_id']); }); } } 3.2 统一授权处理类设计 创建一个统一的OAuth处理类,封装各平台的授权流程: class SMPL_OAuth_Handler { private $config; public function __construct() { $this->config = new SMPL_Platform_Config(); } /** * 生成授权URL */ public function get_auth_url($platform, $state = '') { $config = $this->config->get_platform_config($platform); if (!$config) { return false; } $params = [ 'response_type' => 'code', 'client_id' => $config['app_id'], 'redirect_uri' => $this->get_callback_url($platform), 'state' => $state ?: $this->generate_state(), 'scope' => $config['scope'] ]; // 平台特定参数 if ($platform === 'wechat') { $params['appid'] = $config['app_id']; } return $config['auth_url'] . '?' . http_build_query($params); } /** * 处理授权回调 */ public function handle_callback($platform) { if (!isset($_GET['code']) || !isset($_GET['state'])) { return new WP_Error('invalid_callback', '无效的回调参数'); } $code = sanitize_text_field($_GET['code']); $state = sanitize_text_field($_GET['state']); // 验证state防止CSRF攻击 if (!$this->validate_state($state)) { return new WP_Error('invalid_state', '无效的state参数'); } // 获取访问令牌 $token_data = $this->get_access_token($platform, $code); if (is_wp_error($token_data)) { return $token_data; } // 获取用户信息 $user_info = $this->get_user_info($platform, $token_data['access_token'], isset($token_data['openid']) ? $token_data['openid'] : ''); if (is_wp_error($user_info)) { return $user_info; } // 处理用户登录或注册 return $this->process_user($platform, $user_info, $token_data); } /** * 获取访问令牌 */ private function get_access_token($platform, $code) { $config = $this->config->get_platform_config($platform); $params = [ 'grant_type' => 'authorization_code', 'code' => $code, 'client_id' => $config['app_id'], 'client_secret' => $config['app_secret'], 'redirect_uri' => $this->get_callback_url($platform) ]; $response = wp_remote_post($config['token_url'], [ 'body' => $params, 'timeout' => 15 ]); if (is_wp_error($response)) { return $response; } $body = wp_remote_retrieve_body($response); // 不同平台返回格式不同 switch ($platform) { case 'wechat': case 'weibo': $data = json_decode($body, true); break; case 'qq': parse_str($body, $data); break; case 'github': parse_str($body, $data); if (isset($data['access_token'])) { $data = ['access_token' => $data['access_token']]; } break; default: $data = json_decode($body, true); } if (isset($data['error'])) { return new WP_Error('token_error', $data['error_description'] ?? $data['error']); } return $data; } /** * 获取用户信息 */ private function get_user_info($platform, $access_token, $openid = '') { $config = $this->config->get_platform_config($platform); $params = ['access_token' => $access_token]; // 添加平台特定参数 switch ($platform) { case 'wechat': $params['openid'] = $openid; $params['lang'] = 'zh_CN'; break; case 'qq': $params['oauth_consumer_key'] = $config['app_id']; $params['openid'] = $openid; break; case 'github': // GitHub使用Authorization头 $headers = ['Authorization' => 'token ' . $access_token]; $response = wp_remote_get($config['userinfo_url'], ['headers' => $headers]); break; } if (!isset($response)) { $response = wp_remote_get($config['userinfo_url'] . '?' . http_build_query($params)); } if (is_wp_error($response)) { return $response; } $body = wp_remote_retrieve_body($response); $user_data = json_decode($body, true); if (isset($user_data['error'])) { return new WP_Error('userinfo_error', $user_data['error']); } return $this->normalize_user_data($platform, $user_data); } /** * 标准化用户数据 */ private function normalize_user_data($platform, $data) { $normalized = [ 'platform' => $platform, 'platform_user_id' => '', 'username' => '', 'email' => '', 'display_name' => '', 'avatar' => '' ]; switch ($platform) { case 'wechat': $normalized['platform_user_id'] = $data['openid'] ?? ''; $normalized['display_name'] = $data['nickname'] ?? ''; $normalized['avatar'] = $data['headimgurl'] ?? ''; break; case 'qq': $normalized['platform_user_id'] = $data['openid'] ?? ''; $normalized['display_name'] = $data['nickname'] ?? ''; $normalized['avatar'] = $data['figureurl_qq_2'] ?? $data['figureurl_qq_1'] ?? ''; break; case 'weibo': $normalized['platform_user_id'] = $data['id'] ?? ''; $normalized['display_name'] = $data['screen_name'] ?? ''; $normalized['avatar'] = $data['profile_image_url'] ?? ''; break; case 'github': $normalized['platform_user_id'] = $data['id'] ?? ''; $normalized['username'] = $data['login'] ?? ''; $normalized['display_name'] = $data['name'] ?? $data['login']; $normalized['email'] = $data['email'] ?? ''; $normalized['avatar'] = $data['avatar_url'] ?? ''; break; } return $normalized; } } 3.3 用户账户关联与登录处理 class SMPL_User_Handler { private $oauth_handler; public function __construct() { $this->oauth_handler = new SMPL_OAuth_Handler(); } /** * 处理用户登录或注册 */ public function process_user($platform, $user_info, $token_data) { global $wpdb; $table_name = $wpdb->prefix . 'smpl_user_connections'; // 检查是否已存在关联 $existing = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE platform = %s AND platform_user_id = %s", $platform, $user_info['platform_user_id'] )); if ($existing) { // 更新令牌信息 $wpdb->update( $table_name, [ 'access_token' => $token_data['access_token'], 'refresh_token' => $token_data['refresh_token'] ?? '', 'token_expiry' => isset($token_data['expires_in']) ? date('Y-m-d H:i:s', time() + $token_data['expires_in']) : null, 'user_data' => json_encode($user_info) ], ['id' => $existing->id] ); $user_id = $existing->user_id; } else { // 创建新用户或关联现有用户 if (is_user_logged_in()) { // 已登录用户,关联到当前账户 $user_id = get_current_user_id(); } else { // 创建新用户 $user_id = $this->create_user_from_platform($user_info); } if (is_wp_error($user_id)) { return $user_id; } // 保存关联信息 $wpdb->insert( $table_name, [ 'user_id' => $user_id, 'platform' => $platform, 'platform_user_id' => $user_info['platform_user_id'], 'access_token' => $token_data['access_token'], 'refresh_token' => $token_data['refresh_token'] ?? '', 'token_expiry' => isset($token_data['expires_in']) ? date('Y-m-d H:i:s', time() + $token_data['expires_in']) : null, 'user_data' => json_encode($user_info) ] ); } // 记录登录日志 $this->log_login($user_id, $platform, true); // 执行用户登录 if (!is_user_logged_in()) { wp_set_current_user($user_id); wp_set_auth_cookie($user_id, true); // 更新用户最后登录时间 update_user_meta($user_id, 'last_login', current_time('mysql')); } return $user_id; } /** * 从平台信息创建用户 */ private function create_user_from_platform($user_info) { // 生成唯一用户名 $username = $this->generate_unique_username($user_info); // 生成随机密码 $password = wp_generate_password(12, true, true); // 准备用户数据 $userdata = [ 'user_login' => $username, 'user_pass' => $password, 'display_name' => $user_info['display_name'], 'user_email' => $user_info['email'] ?: $this->generate_temp_email($username), 'role' => get_option('default_role', 'subscriber') ]; // 插入用户 $user_id = wp_insert_user($userdata); if (is_wp_error($user_id)) { return $user_id; } // 保存用户头像 if (!empty($user_info['avatar'])) { update_user_meta($user_id, 'smpl_platform_avatar', $user_info['avatar']); // 可选:下载并设置为本地头像 $this->download_remote_avatar($user_id, $user_info['avatar']); } // 保存平台信息 update_user_meta($user_id, 'smpl_registered_via', $user_info['platform']); // 发送欢迎邮件(可选) if (!empty($user_info['email'])) { wp_new_user_notification($user_id, null, 'user'); } return $user_id; } /** * 生成唯一用户名 */ private function generate_unique_username($user_info) { $base_username = ''; // 尝试使用不同字段作为用户名基础 if (!empty($user_info['username'])) { $base_username = sanitize_user($user_info['username'], true); } elseif (!empty($user_info['display_name'])) { $base_username = sanitize_user($user_info['display_name'], true); } else { $base_username = $user_info['platform'] . '_user'; } // 清理用户名 $base_username = preg_replace('/[^a-z0-9_]/', '', strtolower($base_username)); // 确保唯一性 $username = $base_username; $counter = 1; while (username_exists($username)) { $username = $base_username . $counter; $counter++; } return $username; } /** * 记录登录日志 */ private function log_login($user_id, $platform, $success) { global $wpdb; $table_name = $wpdb->prefix . 'smpl_login_logs'; $wpdb->insert( $table_name, [ 'user_id' => $user_id, 'platform' => $platform, 'ip_address' => $this->get_client_ip(), 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'status' => $success ? 'success' : 'failed' ] ); } /** * 获取客户端IP */ private function get_client_ip() { $ip_keys = ['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR']; foreach ($ip_keys as $key) { if (array_key_exists($key, $_SERVER) === true) { foreach (explode(',', $_SERVER[$key]) as $ip) { $ip = trim($ip); if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) { return $ip; } } } } return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; } } ## 第四章:前端登录界面实现 ### 4.1 登录按钮组件设计 class SMPL_Login_Buttons { private $platform_config; public function __construct() { $this->platform_config = new SMPL_Platform_Config(); } /** * 显示登录按钮 */ public function display_buttons($args = []) { $defaults = [ 'layout' => 'horizontal', // horizontal, vertical, grid 'size' => 'normal', // small, normal, large 'shape' => 'rectangle', // rectangle, rounded, circle 'show_labels' => true, 'show_icons' => true, 'before_text' => '快速登录:', 'container_class' => 'smpl-login-buttons' ]; $args = wp_parse_args($args, $defaults); $platforms = $this->platform_config->get_enabled_platforms(); if (empty($platforms)) { return ''; } ob_start(); ?> <div class="<?php echo esc_attr($args['container_class']); ?> layout-<?php echo esc_attr($args['layout']); ?>"> <?php if ($args['before_text']) : ?> <div class="smpl-before-text"><?php echo esc_html($args['before_text']); ?></div> <?php endif; ?> <div class="smpl-buttons-container"> <?php foreach ($platforms as $platform => $config) : ?> <?php $auth_url = $this->get_auth_url($platform); if (!$auth_url) continue; ?> <a href="<?php echo esc_url($auth_url); ?>" class="smpl-login-button smpl-<?php echo esc_attr($platform); ?> size-<?php echo esc_attr($args['size']); ?> shape-<?php echo esc_attr($args['shape']); ?>" data-platform="<?php echo esc_attr($platform); ?>" title="<?php printf(__('使用%s登录'), esc_attr($config['name'])); ?>"> <?php if ($args['show_icons']) : ?> <span class="smpl-button-icon"> <?php echo $this->get_platform_icon($platform); ?> </span> <?php endif; ?> <?php if ($args['show_labels']) : ?> <span class="smpl-button-text"> <?php printf(__('%s登录'), esc_html($config['name'])); ?> </span> <?php endif; ?> </a> <?php endforeach; ?> </div> </div> <?php // 添加CSS样式 $this->add_styles($args); return ob_get_clean(); } /** * 获取平台图标 */ private function get_platform_icon($platform) { $icons = [ 'wechat' => '<svg viewBox="0 0 24 24"><path d="M9.5,4C5.4,4,2,7.4,2,11.5c0,1.7,0.7,3.4,1.9,4.6l-0.7,2.6l2.7-0.7c1.2,0.7,2.6,1.1,4,1.1c4.1,0,7.5-3.4,7.5-7.5S13.6,4,9.5,4z"/></svg>', 'qq' => '<svg viewBox="0 0 24 24"><path d="M12,2C6.5,2,2,6.5,2,12c0,1.7,0.5,3.4,1.3,4.8l-1.1,4.1l4.2-1.1c1.4,0.8,3,1.2,4.6,1.2c5.5,0,10-4.5,10-10S17.5,2,12,2z"/></svg>', 'weibo' => '<svg viewBox="0 0 24 24"><path d="M20,12c0,4.4-3.6,8-8,8s-8-3.6-8-8s3.6-8,8-8S20,7.6,20,12z M10.9,9.2c-0.6,0-1.1,0.5-1.1,1.1s0.5,1.1,1.1,1.1s1.1-0.5,1.1-1.1S11.5,9.2,10.9,9.2z M13.1,14.8c-1.2,0-2.2-1-2.2-2.2s1-2.2,2.2-2.2s2.2,1,2.2,2.2S14.3,14.8,13.1,14.8z"/></svg>', 'github' => '<svg viewBox="0 0 24 24"><path d="M12,2C6.5,2,2,6.5,2,12c0,4.4,2.9,8.1,6.9,9.4c0.5,0.1,0.7-0.2,0.7-0.5c0-0.2,0-1,0-2c-2.8,0.6-3.4-1.3-3.4-1.3c-0.5-1.2-1.1-1.5-1.1-1.5c-0.9-0.6,0.1-0.6,0.1-0.6c1,0.1,1.5,1,1.5,1c0.9,1.5,2.3,1.1,2.9,0.8c0.1-0.6,0.3-1.1,0.6-1.4c-2.2-0.3-4.6-1.1-4.6-5c0-1.1,0.4-2,1-2.7c-0.1-0.3-0.4-1.3,0.1-2.7c0,0,0.8-0.3,2.7,1c0.8-0.2,1.6-0.3,2.4-0.3c0.8,0,1.6,0.1,2.4,0.3c1.9-1.3,2.7-1,2.7-1c0.5,1.4,0.2,2.4,0.1,2.7c0.6,0.7,1,1.6,1,2.7c0,3.9-2.3,4.7-4.6,4.9c0.4,0.3,0.7,1,0.7,2c0,1.4,0,2.6,0,2.9c0,0.3,0.2,0.6,0.7,0.5c4-1.3,6.9-5,6.9-9.4C22,6.5,17.5,2,12,2z"/></svg>' ]; return isset($icons[$platform]) ? $icons[$platform] : ''; } /** * 添加CSS样式 */ private function add_styles($args) { static $styles_added = false; if ($styles_added) { return; } $styles = " <style> .smpl-login-buttons { margin: 20px 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } .smpl-before-text { margin-bottom: 10px; color: #666; font-size: 14px; } .smpl-buttons-container { display: flex; gap: 10px; flex-wrap: wrap; } .smpl-login-buttons.layout-horizontal .smpl-buttons-container { flex-direction: row; } .smpl-login-buttons.layout-vertical .smpl-buttons-container { flex-direction: column; align-items: stretch; } .smpl-login-buttons.layout-grid .smpl-buttons-container { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); } .smpl-login-button { display: inline-flex; align-items: center; justify-content: center; text-decoration: none; border: none; cursor: pointer; transition: all 0.3s ease; font-weight: 500; } .smpl-login-button:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); } /* 尺寸 */ .smpl-login-button.size-small { padding: 6px 12px; font-size: 12px; } .smpl-login-button.size-normal { padding: 10px 20px; font-size: 14px; } .smpl-login-button.size-large { padding: 14px 28px; font-size: 16px; } /* 形状 */ .smpl-login-button.shape-rectangle { border-radius: 4px; } .smpl-login-button.shape-rounded { border-radius: 20px; } .smpl-login-button.shape-circle { border-radius: 50%; width: 44px; height: 44px; padding: 0; } .smpl-login-button.shape-circle .smpl-button-text { display: none; } /* 图标 */ .smpl-button-icon { display: inline-flex; align-items: center; margin-right: 8px; } .smpl-button-icon svg { width: 18px; height: 18px; fill: currentColor; } .smpl-login-button.shape-circle .smpl-button-icon { margin-right: 0; } /* 平台特定样式 */ .smpl-wechat { background-color: #07C160; color: white; } .smpl-qq { background-color: #12B7F5; color: white; } .smpl-weibo { background-color: #E6162D; color: white; } .smpl-github { background-color: #24292E; color: white; } /* 响应式设计 */ @media (max-width: 480px) { .smpl-login-buttons.layout-horizontal .smpl-buttons-container { flex-direction: column; } .smpl-login-button { width: 100%; justify-content: center; } } </style> "; echo $styles; $styles_added = true; } } ### 4.2 用户中心账户管理界面 class SMPL_User_Profile { public function __construct() { // 添加用户资料页面的账户管理部分 add_action('show_user_profile', [$this, 'add_profile_section']); add_action('edit_user_profile', [$this, 'add_profile_section']); // 保存用户设置 add_action('personal_options_update', [$this, 'save_profile_settings']); add_action('edit_user_profile_update', [$this, 'save_profile_settings']); // 添加快捷码 add_shortcode('smpl_connected_accounts', [$this, 'connected_accounts_shortcode']); } /** * 添加账户管理部分到用户资料页面 */ public function add_profile_section($user) { if (!current_user_can('edit_user', $user->ID)) { return; } $connected_accounts = $this->get_user_connected_accounts($user->ID); ?> <h3><?php _e('第三方账户管理', 'smpl'); ?></h3> <table class="form-table"> <tr> <th><?php _e('已连接的平台', 'smpl'); ?></th> <td> <?

发表评论

详细指南,在WordPress中开发集成在线简易PSD文件查看与标注工具

详细指南:在WordPress中开发集成在线简易PSD文件查看与标注工具 摘要 本文提供了一份详细的技术指南,介绍如何在WordPress平台中通过代码二次开发,集成一个在线简易PSD文件查看与标注工具。我们将从需求分析开始,逐步讲解技术选型、开发流程、核心功能实现以及优化建议,帮助开发者掌握在WordPress中扩展专业功能的方法。 目录 引言:为什么在WordPress中集成PSD查看与标注工具 技术选型与准备工作 WordPress插件架构设计 前端PSD查看器实现 标注功能开发 用户权限与文件管理 性能优化与安全考虑 测试与部署 扩展功能建议 结论 1. 引言:为什么在WordPress中集成PSD查看与标注工具 1.1 WordPress作为内容管理平台的扩展性 WordPress作为全球最流行的内容管理系统,不仅用于博客和网站建设,其强大的插件机制和可扩展性使其成为各种专业应用的理想平台。通过二次开发,我们可以将专业工具集成到WordPress中,为用户提供一体化的解决方案。 1.2 PSD文件查看与标注的需求场景 对于设计团队、客户协作和在线教育等场景,能够直接在网页中查看PSD文件并进行标注可以极大提高工作效率: 设计师与客户之间的设计评审 团队内部的设计协作 在线设计课程的素材展示 设计稿版本对比与反馈收集 1.3 现有解决方案的局限性 虽然市场上有一些在线设计工具,但它们往往需要付费、功能过于复杂或无法与WordPress无缝集成。通过自主开发,我们可以创建轻量级、定制化的解决方案,完美融入现有WordPress环境。 2. 技术选型与准备工作 2.1 开发环境搭建 在开始开发前,需要准备以下环境: # 本地开发环境 - WordPress 5.8+ 安装 - PHP 7.4+ 环境 - MySQL 5.6+ 或 MariaDB 10.1+ - 代码编辑器(VS Code、PHPStorm等) - 浏览器开发者工具 2.2 核心技术选型 2.2.1 PSD解析库选择 考虑到PSD文件的复杂性,我们需要选择合适的解析库: PSD.js - 基于JavaScript的PSD解析器,适合前端处理 ImageMagick/GraphicsMagick - 服务器端处理方案 Photoshop API - Adobe官方API(功能强大但成本较高) 对于简易查看器,我们推荐使用PSD.js,因为它: 纯前端实现,减轻服务器负担 开源免费,社区活跃 支持图层提取和基本信息读取 2.2.2 标注工具库选择 Fabric.js - 强大的Canvas操作库 Konva.js - 另一个优秀的Canvas库 自定义Canvas实现 - 更轻量但开发成本高 我们选择Fabric.js,因为它提供了丰富的图形对象和交互功能。 2.2.3 WordPress开发框架 我们将采用标准的WordPress插件开发模式: 遵循WordPress编码标准 使用WordPress REST API进行前后端通信 利用WordPress的媒体库进行文件管理 2.3 插件基础结构 创建插件基础目录结构: 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/ # 国际化文件 3. WordPress插件架构设计 3.1 主插件文件结构 <?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); } } 3.2 数据库设计 我们需要创建以下数据库表来存储标注信息: -- 标注数据表 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; 4. 前端PSD查看器实现 4.1 引入必要的JavaScript库 // 在插件中注册脚本 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'); 4.2 PSD文件解析与显示 // 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; } psd-canvas { 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网站能让用户在线协作审查代码、对比不同版本,这不仅能吸引更多开发者用户,还能为现有用户提供强大的实用工具。 传统的代码审查通常需要复杂的开发环境和专业工具,但通过WordPress的灵活性和可扩展性,我们可以将这些专业功能集成到普通网站中。本文将手把手教你如何通过WordPress二次开发,为你的网站添加在线协同代码审查与版本对比功能,无需从头构建复杂系统,利用现有插件和自定义开发实现这一目标。 第一部分:准备工作与环境搭建 1.1 选择合适的WordPress环境 在开始之前,确保你的WordPress环境满足以下要求: WordPress 5.0或更高版本 PHP 7.4或更高版本(推荐PHP 8.0+) MySQL 5.6或更高版本 至少256MB内存限制 支持HTTPS(协同功能需要安全连接) 1.2 必备插件安装 我们将使用一些现有插件作为基础,减少开发工作量: Advanced Custom Fields (ACF) - 用于创建自定义字段和元数据 User Role Editor - 管理用户权限和角色 WP Code Highlight.js - 代码高亮显示 Simple History - 记录操作日志 安装这些插件后,激活并确保它们正常运行。 1.3 创建子主题 为了避免主题更新覆盖我们的修改,建议创建子主题: 在wp-content/themes/目录下创建新文件夹,命名为my-code-review-theme 创建style.css文件,添加以下内容: /* Theme Name: Code Review Child Theme Template: your-parent-theme-folder-name Version: 1.0 */ @import url("../your-parent-theme-folder-name/style.css"); 创建functions.php文件,暂时留空 在WordPress后台启用这个子主题 第二部分:数据库设计与数据模型 2.1 设计代码审查数据结构 我们需要创建自定义数据库表来存储代码审查相关数据。在子主题的functions.php中添加以下代码: // 创建自定义数据库表 function create_code_review_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'code_reviews'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, title varchar(255) NOT NULL, description text, code_content longtext NOT NULL, language varchar(50) DEFAULT 'php', status varchar(20) DEFAULT 'pending', author_id bigint(20) NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 创建评论表 $comments_table = $wpdb->prefix . 'code_review_comments'; $sql2 = "CREATE TABLE IF NOT EXISTS $comments_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, review_id mediumint(9) NOT NULL, user_id bigint(20) NOT NULL, content text NOT NULL, line_number int(11), resolved tinyint(1) DEFAULT 0, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY review_id (review_id) ) $charset_collate;"; dbDelta($sql2); // 创建版本表 $versions_table = $wpdb->prefix . 'code_review_versions'; $sql3 = "CREATE TABLE IF NOT EXISTS $versions_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, review_id mediumint(9) NOT NULL, version_number int(11) NOT NULL, code_content longtext NOT NULL, author_id bigint(20) NOT NULL, change_summary text, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY review_id (review_id) ) $charset_collate;"; dbDelta($sql3); } add_action('after_setup_theme', 'create_code_review_tables'); 2.2 使用ACF创建自定义字段 通过Advanced Custom Fields插件创建代码审查所需的字段组: 在WordPress后台进入ACF -> 字段组 -> 新建 添加以下字段: 代码语言选择器(选择字段) 代码内容(文本区域字段,使用Monaco编辑器样式) 审查状态(选择字段:待审查、进行中、已完成) 允许协作的用户(用户关系字段) 将字段组分配给"代码审查"文章类型(我们将在下一节创建) 第三部分:创建代码审查自定义文章类型 3.1 注册自定义文章类型 在子主题的functions.php中添加以下代码: // 注册代码审查自定义文章类型 function register_code_review_post_type() { $labels = array( 'name' => '代码审查', 'singular_name' => '代码审查', 'menu_name' => '代码审查', 'add_new' => '添加新审查', 'add_new_item' => '添加新代码审查', 'edit_item' => '编辑代码审查', 'new_item' => '新代码审查', 'view_item' => '查看代码审查', 'search_items' => '搜索代码审查', 'not_found' => '未找到代码审查', 'not_found_in_trash' => '回收站中无代码审查' ); $args = array( 'labels' => $labels, 'public' => true, 'publicly_queryable' => true, 'show_ui' => true, 'show_in_menu' => true, 'query_var' => true, 'rewrite' => array('slug' => 'code-review'), 'capability_type' => 'post', 'has_archive' => true, 'hierarchical' => false, 'menu_position' => 5, 'menu_icon' => 'dashicons-editor-code', 'supports' => array('title', 'editor', 'author', 'comments'), 'show_in_rest' => true, // 启用Gutenberg编辑器支持 ); register_post_type('code_review', $args); } add_action('init', 'register_code_review_post_type'); 3.2 添加自定义分类法 为代码审查添加分类,如编程语言、项目类型等: // 注册代码审查分类法 function register_code_review_taxonomies() { // 编程语言分类 $language_labels = array( 'name' => '编程语言', 'singular_name' => '编程语言', 'search_items' => '搜索编程语言', 'all_items' => '所有编程语言', 'parent_item' => '父级编程语言', 'parent_item_colon' => '父级编程语言:', 'edit_item' => '编辑编程语言', 'update_item' => '更新编程语言', 'add_new_item' => '添加新编程语言', 'new_item_name' => '新编程语言名称', 'menu_name' => '编程语言', ); $language_args = array( 'hierarchical' => true, 'labels' => $language_labels, 'show_ui' => true, 'show_admin_column' => true, 'query_var' => true, 'rewrite' => array('slug' => 'code-language'), 'show_in_rest' => true, ); register_taxonomy('code_language', array('code_review'), $language_args); } add_action('init', 'register_code_review_taxonomies'); 第四部分:前端代码编辑器集成 4.1 集成Monaco代码编辑器 Monaco是VS Code使用的编辑器,功能强大。我们将它集成到WordPress中: // 添加Monaco编辑器资源 function enqueue_code_editor_assets() { if (is_singular('code_review') || is_post_type_archive('code_review')) { // Monaco Editor wp_enqueue_script('monaco-editor', 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/loader.min.js', array(), '0.34.0', true); // 自定义编辑器脚本 wp_enqueue_script('code-review-editor', get_stylesheet_directory_uri() . '/js/code-editor.js', array('jquery', 'monaco-editor'), '1.0', true); // 编辑器样式 wp_enqueue_style('code-review-style', get_stylesheet_directory_uri() . '/css/code-review.css', array(), '1.0'); // 传递数据到JavaScript wp_localize_script('code-review-editor', 'codeReviewData', array( 'postId' => get_the_ID(), 'userId' => get_current_user_id(), 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('code_review_nonce') )); } } add_action('wp_enqueue_scripts', 'enqueue_code_editor_assets'); 4.2 创建编辑器前端界面 创建/js/code-editor.js文件: (function($) { 'use strict'; // 等待Monaco编辑器加载 require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs' } }); $(document).ready(function() { // 初始化代码编辑器 if ($('#code-editor-container').length) { initCodeEditor(); } // 初始化版本对比功能 if ($('#diff-editor-container').length) { initDiffEditor(); } }); function initCodeEditor() { require(['vs/editor/editor.main'], function() { // 获取代码内容 var initialCode = $('#code-content').val() || '// 在这里输入你的代码nconsole.log("Hello, World!");'; var language = $('#code-language').val() || 'javascript'; // 创建编辑器实例 window.codeEditor = monaco.editor.create(document.getElementById('code-editor-container'), { value: initialCode, language: language, theme: 'vs-dark', fontSize: 14, minimap: { enabled: true }, scrollBeyondLastLine: false, automaticLayout: true }); // 监听内容变化 window.codeEditor.onDidChangeModelContent(function() { $('#code-content').val(window.codeEditor.getValue()); }); // 语言切换 $('#code-language').on('change', function() { var language = $(this).val(); monaco.editor.setModelLanguage(window.codeEditor.getModel(), language); }); }); } function initDiffEditor() { require(['vs/editor/editor.main'], function() { // 获取对比的代码版本 var originalCode = $('#original-code').val() || ''; var modifiedCode = $('#modified-code').val() || ''; var language = $('#diff-language').val() || 'javascript'; // 创建对比编辑器 window.diffEditor = monaco.editor.createDiffEditor(document.getElementById('diff-editor-container'), { theme: 'vs-dark', fontSize: 14, readOnly: true, automaticLayout: true }); // 设置对比模型 var originalModel = monaco.editor.createModel(originalCode, language); var modifiedModel = monaco.editor.createModel(modifiedCode, language); window.diffEditor.setModel({ original: originalModel, modified: modifiedModel }); }); } // 保存代码版本 window.saveCodeVersion = function() { var codeContent = window.codeEditor ? window.codeEditor.getValue() : ''; var changeSummary = prompt('请输入本次更改的摘要:', ''); if (changeSummary === null) return; $.ajax({ url: codeReviewData.ajaxUrl, type: 'POST', data: { action: 'save_code_version', post_id: codeReviewData.postId, code_content: codeContent, change_summary: changeSummary, nonce: codeReviewData.nonce }, success: function(response) { if (response.success) { alert('版本保存成功!'); location.reload(); } else { alert('保存失败: ' + response.data); } } }); }; // 添加行内评论 window.addLineComment = function(lineNumber) { var commentText = prompt('请输入对第 ' + lineNumber + ' 行的评论:', ''); if (commentText === null || commentText.trim() === '') return; $.ajax({ url: codeReviewData.ajaxUrl, type: 'POST', data: { action: 'add_line_comment', post_id: codeReviewData.postId, line_number: lineNumber, comment: commentText, nonce: codeReviewData.nonce }, success: function(response) { if (response.success) { alert('评论添加成功!'); location.reload(); } else { alert('添加失败: ' + response.data); } } }); }; })(jQuery); 第五部分:版本对比功能实现 5.1 创建版本对比界面 在子主题中创建code-review-diff.php模板文件: <?php /** * 代码版本对比模板 */ get_header(); ?> <div class="container code-review-container"> <div class="row"> <div class="col-md-12"> <h1 class="page-title">代码版本对比</h1> <?php $review_id = isset($_GET['review_id']) ? intval($_GET['review_id']) : 0; $version1 = isset($_GET['v1']) ? intval($_GET['v1']) : 0; $version2 = isset($_GET['v2']) ? intval($_GET['v2']) : 0; if ($review_id && $version1 && $version2) { global $wpdb; $versions_table = $wpdb->prefix . 'code_review_versions'; // 获取版本1内容 $version1_data = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $versions_table WHERE id = %d AND review_id = %d", $version1, $review_id )); // 获取版本2内容 $version2_data = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $versions_table WHERE id = %d AND review_id = %d", $version2, $review_id )); if ($version1_data && $version2_data) { ?> <div class="diff-info"> <h3>对比版本 <?php echo $version1_data->version_number; ?> 与版本 <?php echo $version2_data->version_number; ?></h3> <p><strong>版本<?php echo $version1_data->version_number; ?>:</strong> <?php echo esc_html($version1_data->change_summary); ?> (<?php echo date('Y-m-d H:i', strtotime($version1_data->created_at)); ?>)</p> <p><strong>版本<?php echo $version2_data->version_number; ?>:</strong> <?php echo esc_html($version2_data->change_summary); ?> (<?php echo date('Y-m-d H:i', strtotime($version2_data->created_at)); ?>)</p> </div> <div class="diff-container"> <div id="diff-editor-container" style="height: 600px; border: 1px solid #ddd;"></div> <textarea id="original-code" style="display:none;"><?php echo esc_textarea($version1_data->code_content); ?></textarea> <textarea id="modified-code" style="display:none;"><?php echo esc_textarea($version2_data->code_content); ?></textarea> <input type="hidden" id="diff-language" value="php"> </div> <div class="diff-actions mt-3"> <a href="<?php echo get_permalink($review_id); ?>" class="btn btn-secondary">返回代码审查</a> <button onclick="window.print()" class="btn btn-info">打印对比结果</button> </div> <?php } else { echo '<div class="alert alert-danger">未找到指定的版本数据。</div>'; } } else { echo '<div class="alert alert-warning">请选择要对比的版本。</div>'; } ?> </div> </div> </div> <?php get_footer(); ?> 5.2 实现版本对比算法 创建/includes/diff-functions.php文件,实现简单的行级对比: <?php /** * 代码对比功能函数 */ // 简单的行级对比函数 function compare_code_versions($code1, $code2) { $lines1 = explode("n", $code1); $lines2 = explode("n", $code2); $diff = array(); $maxLines = max(count($lines1), count($lines2)); 0; $i < $maxLines; $i++) { $line1 = isset($lines1[$i]) ? $lines1[$i] : ''; $line2 = isset($lines2[$i]) ? $lines2[$i] : ''; if ($line1 !== $line2) { $diff[] = array( 'line' => $i + 1, 'original' => $line1, 'modified' => $line2, 'type' => $line1 === '' ? 'added' : ($line2 === '' ? 'removed' : 'changed') ); } } return $diff; } // 生成对比HTMLfunction generate_diff_html($code1, $code2, $language = 'php') { $diff = compare_code_versions($code1, $code2); if (empty($diff)) { return '<div class="alert alert-success">两个版本完全相同</div>'; } $html = '<div class="code-diff-view">'; $html .= '<table class="diff-table table table-bordered">'; $html .= '<thead><tr><th width="5%">行号</th><th width="45%">版本A</th><th width="45%">版本B</th><th width="5%">状态</th></tr></thead>'; $html .= '<tbody>'; foreach ($diff as $change) { $status_class = ''; $status_text = ''; switch ($change['type']) { case 'added': $status_class = 'diff-added'; $status_text = '+'; break; case 'removed': $status_class = 'diff-removed'; $status_text = '-'; break; case 'changed': $status_class = 'diff-changed'; $status_text = '~'; break; } $html .= '<tr class="' . $status_class . '">'; $html .= '<td class="line-number">' . $change['line'] . '</td>'; $html .= '<td class="original-line"><code>' . htmlspecialchars($change['original']) . '</code></td>'; $html .= '<td class="modified-line"><code>' . htmlspecialchars($change['modified']) . '</code></td>'; $html .= '<td class="diff-status">' . $status_text . '</td>'; $html .= '</tr>'; } $html .= '</tbody></table></div>'; return $html; } // 获取版本历史function get_code_version_history($review_id) { global $wpdb; $versions_table = $wpdb->prefix . 'code_review_versions'; $versions = $wpdb->get_results($wpdb->prepare( "SELECT v.*, u.display_name as author_name FROM $versions_table v LEFT JOIN {$wpdb->users} u ON v.author_id = u.ID WHERE v.review_id = %d ORDER BY v.version_number DESC", $review_id )); return $versions; }?> ## 第六部分:协同审查功能实现 ### 6.1 实时评论系统 创建实时评论功能,允许用户在特定代码行添加评论: // 在functions.php中添加AJAX处理函数add_action('wp_ajax_add_line_comment', 'handle_add_line_comment');add_action('wp_ajax_nopriv_add_line_comment', 'handle_add_line_comment_no_priv'); function handle_add_line_comment() { // 验证nonce if (!wp_verify_nonce($_POST['nonce'], 'code_review_nonce')) { wp_die('安全验证失败'); } // 检查用户权限 if (!is_user_logged_in()) { wp_send_json_error('请先登录'); } $post_id = intval($_POST['post_id']); $line_number = intval($_POST['line_number']); $comment = sanitize_textarea_field($_POST['comment']); $user_id = get_current_user_id(); // 检查用户是否有权限评论 $allowed_users = get_field('allowed_collaborators', $post_id); $is_allowed = false; if ($allowed_users) { foreach ($allowed_users as $allowed_user) { if ($allowed_user['ID'] == $user_id) { $is_allowed = true; break; } } } // 如果是作者或管理员,也允许评论 $post = get_post($post_id); if ($post->post_author == $user_id || current_user_can('manage_options')) { $is_allowed = true; } if (!$is_allowed) { wp_send_json_error('您没有权限在此代码审查中添加评论'); } // 保存评论到数据库 global $wpdb; $comments_table = $wpdb->prefix . 'code_review_comments'; $result = $wpdb->insert( $comments_table, array( 'review_id' => $post_id, 'user_id' => $user_id, 'content' => $comment, 'line_number' => $line_number, 'resolved' => 0, 'created_at' => current_time('mysql') ), array('%d', '%d', '%s', '%d', '%d', '%s') ); if ($result) { // 发送通知邮件 send_comment_notification($post_id, $user_id, $comment, $line_number); wp_send_json_success('评论添加成功'); } else { wp_send_json_error('评论保存失败'); } } function handle_add_line_comment_no_priv() { wp_send_json_error('请先登录'); } // 发送评论通知function send_comment_notification($post_id, $commenter_id, $comment, $line_number) { $post = get_post($post_id); $commenter = get_userdata($commenter_id); $author = get_userdata($post->post_author); $subject = '您的代码审查有新的评论'; $message = "您好 " . $author->display_name . ",nn"; $message .= $commenter->display_name . " 在您的代码审查中添加了评论:nn"; $message .= "代码行号: " . $line_number . "n"; $message .= "评论内容: " . $comment . "nn"; $message .= "查看详情: " . get_permalink($post_id) . "nn"; $message .= "此邮件由系统自动发送,请勿回复。"; wp_mail($author->user_email, $subject, $message); } ### 6.2 评论显示与交互界面 创建评论显示模板: // 在single-code_review.php模板中添加评论显示function display_code_comments($review_id) { global $wpdb; $comments_table = $wpdb->prefix . 'code_review_comments'; $comments = $wpdb->get_results($wpdb->prepare( "SELECT c.*, u.display_name, u.user_email, u.user_nicename FROM $comments_table c LEFT JOIN {$wpdb->users} u ON c.user_id = u.ID WHERE c.review_id = %d ORDER BY c.line_number, c.created_at", $review_id )); if (empty($comments)) { return '<div class="no-comments">暂无评论</div>'; } // 按行号分组 $grouped_comments = array(); foreach ($comments as $comment) { $line = $comment->line_number ?: 'general'; if (!isset($grouped_comments[$line])) { $grouped_comments[$line] = array(); } $grouped_comments[$line][] = $comment; } $html = '<div class="code-comments-section">'; $html .= '<h3>代码评论</h3>'; foreach ($grouped_comments as $line_number => $line_comments) { $line_label = ($line_number === 'general') ? '通用评论' : '第 ' . $line_number . ' 行'; $html .= '<div class="comment-group" data-line="' . $line_number . '">'; $html .= '<h4 class="comment-group-title">' . $line_label . '</h4>'; foreach ($line_comments as $comment) { $resolved_class = $comment->resolved ? 'resolved' : ''; $avatar = get_avatar($comment->user_email, 32); $html .= '<div class="comment-item ' . $resolved_class . '" id="comment-' . $comment->id . '">'; $html .= '<div class="comment-header">'; $html .= '<div class="comment-author">' . $avatar . ' <strong>' . $comment->display_name . '</strong></div>'; $html .= '<div class="comment-meta">' . date('Y-m-d H:i', strtotime($comment->created_at)) . '</div>'; $html .= '</div>'; $html .= '<div class="comment-content">' . nl2br(esc_html($comment->content)) . '</div>'; // 评论操作按钮 if (current_user_can('manage_options') || get_current_user_id() == $comment->user_id) { $html .= '<div class="comment-actions">'; if (!$comment->resolved) { $html .= '<button class="btn-resolve-comment btn btn-sm btn-success" data-comment-id="' . $comment->id . '">标记为已解决</button>'; } else { $html .= '<button class="btn-unresolve-comment btn btn-sm btn-warning" data-comment-id="' . $comment->id . '">重新打开</button>'; } $html .= '<button class="btn-delete-comment btn btn-sm btn-danger" data-comment-id="' . $comment->id . '">删除</button>'; $html .= '</div>'; } $html .= '</div>'; } $html .= '</div>'; } $html .= '</div>'; return $html; } ## 第七部分:用户权限与协作管理 ### 7.1 自定义用户角色与权限 使用User Role Editor插件或代码创建自定义角色: // 创建代码审查者角色function create_code_reviewer_role() { // 复制贡献者角色作为基础 $contributor = get_role('contributor'); // 添加代码审查者角色 add_role('code_reviewer', '代码审查者', $contributor->capabilities); // 获取代码审查者角色并添加额外权限 $reviewer = get_role('code_reviewer'); // 添加自定义文章类型相关权限 $reviewer->add_cap('edit_code_reviews'); $reviewer->add_cap('edit_others_code_reviews'); $reviewer->add_cap('publish_code_reviews'); $reviewer->add_cap('read_private_code_reviews'); $reviewer->add_cap('delete_code_reviews'); // 添加自定义分类法权限 $reviewer->add_cap('manage_code_languages'); $reviewer->add_cap('edit_code_languages'); $reviewer->add_cap('delete_code_languages'); $reviewer->add_cap('assign_code_languages'); }add_action('init', 'create_code_reviewer_role'); // 设置默认权限function set_default_code_review_permissions() { // 给管理员和编辑者添加权限 $admin = get_role('administrator'); $editor = get_role('editor'); $caps = array( 'edit_code_reviews', 'edit_others_code_reviews', 'publish_code_reviews', 'read_private_code_reviews', 'delete_code_reviews', 'manage_code_languages', 'edit_code_languages', 'delete_code_languages', 'assign_code_languages' ); foreach ($caps as $cap) { if ($admin) $admin->add_cap($cap); if ($editor) $editor->add_cap($cap); } }add_action('admin_init', 'set_default_code_review_permissions'); ### 7.2 协作邀请系统 创建用户邀请功能: // 添加协作邀请功能function add_collaborator_invitation($review_id, $invitee_email, $inviter_id) { // 检查用户是否存在 $invitee = get_user_by('email', $invitee_email); if ($invitee) { // 用户已存在,直接添加到允许列表 $allowed_users = get_field('allowed_collaborators', $review_id); if (!$allowed_users) { $allowed_users = array(); } // 检查是否已存在 $already_added = false; foreach ($allowed_users as $user) { if ($user['ID'] == $invitee->ID) { $already_added = true; break; } } if (!$already_added) { $allowed_users[] = array('ID' => $invitee->ID); update_field('allowed_collaborators', $allowed_users, $review_id); // 发送通知 send_invitation_notification($review_id, $invitee->ID, $inviter_id, false); return array('success' => true, 'message' => '用户已添加到协作列表'); } else { return array('success' => false, 'message' => '用户已在协作列表中'); } } else { // 用户不存在,创建邀请记录 global $wpdb; $invites_table = $wpdb->prefix . 'code_review_invites'; // 创建表(如果不存在) $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE IF NOT EXISTS $invites_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, review_id mediumint(9) NOT NULL, invitee_email varchar(100) NOT NULL, inviter_id bigint(20) NOT NULL, token varchar(64) NOT NULL, status varchar(20) DEFAULT 'pending', created_at datetime DEFAULT CURRENT_TIMESTAMP, expires_at datetime, PRIMARY KEY (id), UNIQUE KEY token (token) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 生成唯一token $token = wp_generate_password(64, false); // 设置过期时间(7天后) $expires = date('Y-m-d H:i:s', strtotime('+7 days')); // 保存邀请 $wpdb->insert( $invites_table, array( 'review_id' => $review_id, 'invitee_email' => $invitee_email, 'inviter_id' => $inviter_id, 'token' => $token, 'expires_at' => $expires ), array('%d', '%s', '%d', '%s', '%s') ); // 发送邀请邮件 send_invitation_email($invitee_email, $review_id, $token, $inviter_id); return array('success' => true, 'message' => '邀请已发送到 ' . $invitee_email); } } // 发送邀请邮件function send_invitation_email($email, $review_id, $token, $inviter_id) { $review = get_post($review_id); $inviter = get_userdata($inviter_id); $site_name = get_bloginfo('name'); $invite_link = add_query_arg(array( 'action' => 'accept_invite', 'token' => $token, 'review_id' => $review_id ), home_url('/')); $subject = '您被邀请参与代码审查: ' . $review->post_title; $message = "您好,nn"; $message .= $inviter->display_name . " 邀请您参与代码审查: " . $review->post_title . "nn"; $message .= "审查描述: " . wp_trim_words($review->post_content, 50) . "nn"; $message .= "点击以下链接接受邀请:n"; $message .= $invite_link . "nn"; $message .= "此链接7天内有效。nn"; $message .= "如果这不是您的邮箱,请忽略此邮件。nn"; $message .= "此邮件由 " . $site_name . " 系统自动发送。"; wp_mail($email, $subject, $message); } ## 第八部分:前端界面优化与用户体验 ### 8.1 响应式设计CSS 创建`/css/code-review.css`文件: / 代码审查系统样式 / / 主容器 /.code-review-container { padding: 20px 0; max-width: 1200px; margin: 0 auto; } / 编辑器容器 / code-editor-container, #diff-editor-container { border: 1px solid #ddd; border-radius: 4px; overflow: hidden; } / 代码行号 /.code-line-numbers { font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 14px; line-height: 1.5; color: #999; text-align: right; padding-right: 10px; user-select: none; } / 评论标记 /.line-comment-marker { display: inline-block; width: 20px; height: 20px; background-color: #ffc107; color: #333; border-radius: 50%; text-align: center; line-height: 20

发表评论

WordPress 插件开发教程,集成网站实时公交到站查询与路线规划工具

WordPress插件开发教程:集成实时公交到站查询与路线规划工具 引言:为什么要在WordPress中集成公交查询功能? 在当今数字化时代,网站的功能性已成为吸引和留住访客的关键因素。对于地方门户、旅游网站、企业通勤信息页面或社区服务平台而言,集成实时公交信息可以显著提升用户体验和网站实用性。通过WordPress插件开发,我们可以将复杂的公交查询功能无缝集成到网站中,而无需依赖第三方服务的高昂费用或功能限制。 本教程将引导您从零开始开发一个功能完整的WordPress插件,实现实时公交到站查询与路线规划工具。我们将采用模块化开发方法,确保代码的可维护性和扩展性,同时遵循WordPress开发最佳实践。 第一部分:开发环境准备与插件基础架构 1.1 开发环境配置 在开始开发之前,我们需要准备以下环境: 本地开发环境:推荐使用XAMPP、MAMP或Local by Flywheel 代码编辑器:VS Code、PHPStorm或Sublime Text WordPress安装:最新版本的WordPress(建议5.6以上) 浏览器开发者工具:用于调试JavaScript和API调用 1.2 创建插件基础结构 首先,在WordPress的wp-content/plugins目录下创建一个新文件夹,命名为real-time-bus-query。在该文件夹中创建以下基础文件: real-time-bus-query/ ├── real-time-bus-query.php # 主插件文件 ├── includes/ # 包含核心功能文件 │ ├── class-bus-api.php # API处理类 │ ├── class-shortcodes.php # 短代码处理类 │ └── class-admin-settings.php # 管理设置类 ├── assets/ # 静态资源 │ ├── css/ │ │ └── frontend.css │ ├── js/ │ │ ├── frontend.js │ │ └── admin.js │ └── images/ ├── templates/ # 前端模板 │ ├── bus-query-form.php │ ├── bus-results.php │ └── route-planning.php └── languages/ # 国际化文件 1.3 编写插件主文件 打开real-time-bus-query.php,添加以下代码: <?php /** * Plugin Name: 实时公交查询与路线规划工具 * Plugin URI: https://yourwebsite.com/real-time-bus-query * Description: 在WordPress网站中集成实时公交到站查询与路线规划功能 * Version: 1.0.0 * Author: 您的名称 * Author URI: https://yourwebsite.com * License: GPL v2 or later * Text Domain: real-time-bus-query * Domain Path: /languages */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('RTBQ_VERSION', '1.0.0'); define('RTBQ_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('RTBQ_PLUGIN_URL', plugin_dir_url(__FILE__)); define('RTBQ_PLUGIN_BASENAME', plugin_basename(__FILE__)); // 自动加载类文件 spl_autoload_register(function ($class_name) { $prefix = 'RTBQ_'; $base_dir = RTBQ_PLUGIN_DIR . 'includes/'; // 检查类是否使用我们的命名空间前缀 $len = strlen($prefix); if (strncmp($prefix, $class_name, $len) !== 0) { return; } $relative_class = substr($class_name, $len); $file = $base_dir . 'class-' . str_replace('_', '-', strtolower($relative_class)) . '.php'; if (file_exists($file)) { require $file; } }); // 初始化插件 function rtbq_init() { // 加载文本域用于国际化 load_plugin_textdomain('real-time-bus-query', false, dirname(RTBQ_PLUGIN_BASENAME) . '/languages'); // 初始化各个组件 if (is_admin()) { new RTBQ_Admin_Settings(); } new RTBQ_Shortcodes(); new RTBQ_Bus_API(); } add_action('plugins_loaded', 'rtbq_init'); // 插件激活时执行的操作 function rtbq_activate() { // 创建必要的数据库表 global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'rtbq_cache'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, cache_key varchar(255) NOT NULL, cache_value longtext NOT NULL, expiration datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY cache_key (cache_key) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 添加默认选项 add_option('rtbq_api_provider', 'gaode'); // 默认使用高德地图API add_option('rtbq_api_key', ''); add_option('rtbq_cache_duration', 300); // 默认缓存5分钟 add_option('rtbq_default_city', '北京'); } register_activation_hook(__FILE__, 'rtbq_activate'); // 插件停用时执行的操作 function rtbq_deactivate() { // 清理定时任务 wp_clear_scheduled_hook('rtbq_clear_expired_cache'); } register_deactivation_hook(__FILE__, 'rtbq_deactivate'); 第二部分:公交数据API集成与处理 2.1 选择公交数据API提供商 目前国内可用的公交数据API主要包括: 高德地图API:提供全面的公交线路、站点和实时到站信息 百度地图API:功能类似高德,覆盖范围广泛 腾讯地图API:提供基础公交查询功能 本地公交公司API:某些城市公交公司提供官方API 本教程以高德地图API为例,但代码设计为可扩展,方便切换API提供商。 2.2 创建API处理类 在includes/class-bus-api.php中创建API处理类: <?php class RTBQ_Bus_API { private $api_key; private $api_provider; private $cache_duration; public function __construct() { $this->api_key = get_option('rtbq_api_key', ''); $this->api_provider = get_option('rtbq_api_provider', 'gaode'); $this->cache_duration = get_option('rtbq_cache_duration', 300); // 添加缓存清理定时任务 if (!wp_next_scheduled('rtbq_clear_expired_cache')) { wp_schedule_event(time(), 'hourly', 'rtbq_clear_expired_cache'); } add_action('rtbq_clear_expired_cache', array($this, 'clear_expired_cache')); // 注册REST API端点 add_action('rest_api_init', array($this, 'register_rest_routes')); } /** * 注册REST API路由 */ public function register_rest_routes() { register_rest_route('rtbq/v1', '/search-station', array( 'methods' => 'GET', 'callback' => array($this, 'rest_search_station'), 'permission_callback' => '__return_true', 'args' => array( 'keyword' => array( 'required' => true, 'validate_callback' => function($param) { return !empty(trim($param)); } ), 'city' => array( 'required' => false, 'default' => get_option('rtbq_default_city', '北京') ) ) )); register_rest_route('rtbq/v1', '/bus-lines', array( 'methods' => 'GET', 'callback' => array($this, 'rest_get_bus_lines'), 'permission_callback' => '__return_true', 'args' => array( 'station_id' => array( 'required' => true, 'validate_callback' => function($param) { return !empty(trim($param)); } ), 'city' => array( 'required' => false, 'default' => get_option('rtbq_default_city', '北京') ) ) )); register_rest_route('rtbq/v1', '/route-plan', array( 'methods' => 'GET', 'callback' => array($this, 'rest_route_plan'), 'permission_callback' => '__return_true', 'args' => array( 'origin' => array( 'required' => true, 'validate_callback' => function($param) { return !empty(trim($param)); } ), 'destination' => array( 'required' => true, 'validate_callback' => function($param) { return !empty(trim($param)); } ), 'city' => array( 'required' => false, 'default' => get_option('rtbq_default_city', '北京') ) ) )); } /** * 搜索公交站点 */ public function rest_search_station($request) { $keyword = sanitize_text_field($request->get_param('keyword')); $city = sanitize_text_field($request->get_param('city')); $cache_key = 'rtbq_station_search_' . md5($keyword . $city); $cached_result = $this->get_cache($cache_key); if ($cached_result !== false) { return rest_ensure_response($cached_result); } $result = $this->search_station($keyword, $city); if (!is_wp_error($result)) { $this->set_cache($cache_key, $result); } return rest_ensure_response($result); } /** * 根据API提供商搜索站点 */ private function search_station($keyword, $city) { switch ($this->api_provider) { case 'gaode': return $this->search_station_gaode($keyword, $city); case 'baidu': return $this->search_station_baidu($keyword, $city); default: return new WP_Error('invalid_provider', '不支持的API提供商'); } } /** * 使用高德地图API搜索站点 */ private function search_station_gaode($keyword, $city) { $url = 'https://restapi.amap.com/v3/place/text'; $params = array( 'key' => $this->api_key, 'keywords' => $keyword, 'city' => $city, 'types' => '150700', // 公交站点类型代码 'offset' => 20, 'page' => 1, 'extensions' => 'all' ); $response = wp_remote_get(add_query_arg($params, $url)); if (is_wp_error($response)) { return $response; } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if ($data['status'] !== '1') { return new WP_Error('api_error', $data['info'] ?? 'API请求失败'); } return $this->format_station_results($data['pois'], 'gaode'); } /** * 获取经过某站点的公交线路 */ public function rest_get_bus_lines($request) { $station_id = sanitize_text_field($request->get_param('station_id')); $city = sanitize_text_field($request->get_param('city')); $cache_key = 'rtbq_bus_lines_' . md5($station_id . $city); $cached_result = $this->get_cache($cache_key); if ($cached_result !== false) { return rest_ensure_response($cached_result); } $result = $this->get_bus_lines($station_id, $city); if (!is_wp_error($result)) { $this->set_cache($cache_key, $result); } return rest_ensure_response($result); } /** * 路线规划 */ public function rest_route_plan($request) { $origin = sanitize_text_field($request->get_param('origin')); $destination = sanitize_text_field($request->get_param('destination')); $city = sanitize_text_field($request->get_param('city')); $cache_key = 'rtbq_route_plan_' . md5($origin . $destination . $city); $cached_result = $this->get_cache($cache_key); if ($cached_result !== false) { return rest_ensure_response($cached_result); } $result = $this->get_route_plan($origin, $destination, $city); if (!is_wp_error($result)) { $this->set_cache($cache_key, $result); } return rest_ensure_response($result); } /** * 使用高德地图API进行公交路线规划 */ private function get_route_plan_gaode($origin, $destination, $city) { $url = 'https://restapi.amap.com/v3/direction/transit/integrated'; $params = array( 'key' => $this->api_key, 'origin' => $origin, 'destination' => $destination, 'city' => $city, 'cityd' => $city, 'extensions' => 'all', 'strategy' => '0', // 最快捷模式 'nightflag' => '0' // 不包含夜班车 ); $response = wp_remote_get(add_query_arg($params, $url)); if (is_wp_error($response)) { return $response; } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if ($data['status'] !== '1') { return new WP_Error('api_error', $data['info'] ?? 'API请求失败'); } return $this->format_route_results($data['route'], 'gaode'); } /** * 格式化站点搜索结果 */ private function format_station_results($pois, $provider) { $formatted = array(); foreach ($pois as $poi) { if ($provider === 'gaode') { $formatted[] = array( 'id' => $poi['id'], 'name' => $poi['name'], 'address' => $poi['address'], 'location' => $poi['location'], 'city' => $poi['cityname'] ?? '', 'district' => $poi['adname'] ?? '' ); } // 可以添加其他API提供商的格式化逻辑 } return $formatted; } /** * 缓存管理方法 */ private function get_cache($key) { global $wpdb; $table_name = $wpdb->prefix . 'rtbq_cache'; $result = $wpdb->get_row($wpdb->prepare( "SELECT cache_value FROM $table_name WHERE cache_key = %s AND expiration > %s", $key, current_time('mysql') )); if ($result) { return json_decode($result->cache_value, true); } return false; } private function set_cache($key, $value) { global $wpdb; $table_name = $wpdb->prefix . 'rtbq_cache'; $expiration = date('Y-m-d H:i:s', time() + $this->cache_duration); $data = array( 'cache_key' => $key, 'cache_value' => json_encode($value), 'expiration' => $expiration ); $format = array('%s', '%s', '%s'); $wpdb->replace($table_name, $data, $format); } /** * 清理过期缓存 */ public function clear_expired_cache() { global $wpdb; $table_name = $wpdb->prefix . 'rtbq_cache'; $wpdb->query($wpdb->prepare( "DELETE FROM $table_name WHERE expiration <= %s", current_time('mysql') )); } } 第三部分:前端界面与用户交互实现 3.1 创建短代码处理器 在includes/class-shortcodes.php中创建短代码处理类: <?php class RTBQ_Shortcodes { public function __construct() { // 注册短代码 add_shortcode('bus_query', array($this, 'bus_query_shortcode')); add_shortcode('route_plan', array($this, 'route_plan_shortcode')); // 注册前端脚本和样式 add_action('wp_enqueue_scripts', array($this, 'enqueue_frontend_assets')); } /** * 公交查询短代码 */ public function bus_query_shortcode($atts) { $atts = shortcode_atts(array( 'title' => '实时公交查询', 'default_city' => get_option('rtbq_default_city', '北京'), 'show_history' => 'true' ## 第三部分:前端界面与用户交互实现(续) ### 3.1 创建短代码处理器(续) <?phpclass RTBQ_Shortcodes { public function __construct() { // 注册短代码 add_shortcode('bus_query', array($this, 'bus_query_shortcode')); add_shortcode('route_plan', array($this, 'route_plan_shortcode')); // 注册前端脚本和样式 add_action('wp_enqueue_scripts', array($this, 'enqueue_frontend_assets')); } /** * 公交查询短代码 */ public function bus_query_shortcode($atts) { $atts = shortcode_atts(array( 'title' => '实时公交查询', 'default_city' => get_option('rtbq_default_city', '北京'), 'show_history' => 'true' ), $atts, 'bus_query'); // 开始输出缓冲 ob_start(); // 包含模板文件 include RTBQ_PLUGIN_DIR . 'templates/bus-query-form.php'; // 返回缓冲内容 return ob_get_clean(); } /** * 路线规划短代码 */ public function route_plan_shortcode($atts) { $atts = shortcode_atts(array( 'title' => '公交路线规划', 'default_city' => get_option('rtbq_default_city', '北京') ), $atts, 'route_plan'); ob_start(); include RTBQ_PLUGIN_DIR . 'templates/route-planning.php'; return ob_get_clean(); } /** * 注册前端资源 */ public function enqueue_frontend_assets() { // 只在需要时加载资源 global $post; if (is_a($post, 'WP_Post') && (has_shortcode($post->post_content, 'bus_query') || has_shortcode($post->post_content, 'route_plan'))) { // 加载CSS wp_enqueue_style( 'rtbq-frontend', RTBQ_PLUGIN_URL . 'assets/css/frontend.css', array(), RTBQ_VERSION ); // 加载JavaScript wp_enqueue_script( 'rtbq-frontend', RTBQ_PLUGIN_URL . 'assets/js/frontend.js', array('jquery'), RTBQ_VERSION, true ); // 本地化脚本,传递数据给JavaScript wp_localize_script('rtbq-frontend', 'rtbq_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'rest_url' => rest_url('rtbq/v1/'), 'nonce' => wp_create_nonce('rtbq_nonce'), 'default_city' => get_option('rtbq_default_city', '北京'), 'strings' => array( 'loading' => __('加载中...', 'real-time-bus-query'), 'no_results' => __('未找到结果', 'real-time-bus-query'), 'search_placeholder' => __('输入站点名称', 'real-time-bus-query'), 'select_station' => __('请选择站点', 'real-time-bus-query') ) )); } } } ### 3.2 创建前端模板文件 在`templates/bus-query-form.php`中创建公交查询表单: <div class="rtbq-container"> <div class="rtbq-header"> <h3><?php echo esc_html($atts['title']); ?></h3> </div> <div class="rtbq-search-section"> <div class="rtbq-search-box"> <div class="rtbq-input-group"> <label for="rtbq-city-select"><?php _e('城市', 'real-time-bus-query'); ?>:</label> <select id="rtbq-city-select" class="rtbq-city-select"> <option value="北京">北京</option> <option value="上海">上海</option> <option value="广州">广州</option> <option value="深圳">深圳</option> <option value="杭州">杭州</option> <option value="南京">南京</option> <option value="武汉">武汉</option> <option value="成都">成都</option> <option value="重庆">重庆</option> <option value="天津">天津</option> </select> </div> <div class="rtbq-input-group"> <label for="rtbq-station-search"><?php _e('站点搜索', 'real-time-bus-query'); ?>:</label> <div class="rtbq-search-wrapper"> <input type="text" id="rtbq-station-search" class="rtbq-station-search" placeholder="<?php _e('输入站点名称', 'real-time-bus-query'); ?>" autocomplete="off"> <button type="button" class="rtbq-search-btn"> <span class="dashicons dashicons-search"></span> </button> </div> <div id="rtbq-search-results" class="rtbq-search-results"></div> </div> </div> </div> <div class="rtbq-results-section" style="display: none;"> <div class="rtbq-station-info"> <h4 id="rtbq-current-station"></h4> <p id="rtbq-station-address"></p> </div> <div class="rtbq-bus-lines"> <h5><?php _e('经过该站点的公交线路', 'real-time-bus-query'); ?></h5> <div id="rtbq-bus-list" class="rtbq-bus-list"> <!-- 公交线路将通过JavaScript动态加载 --> </div> </div> <div class="rtbq-real-time-info"> <h5><?php _e('实时到站信息', 'real-time-bus-query'); ?></h5> <div id="rtbq-realtime-data" class="rtbq-realtime-data"> <!-- 实时信息将通过JavaScript动态加载 --> </div> </div> </div> <?php if ($atts['show_history'] === 'true') : ?> <div class="rtbq-history-section"> <h5><?php _e('最近查询', 'real-time-bus-query'); ?></h5> <div id="rtbq-query-history" class="rtbq-query-history"> <!-- 查询历史将通过JavaScript动态加载 --> </div> </div> <?php endif; ?> <div class="rtbq-loading" style="display: none;"> <div class="rtbq-spinner"></div> <p><?php _e('加载中...', 'real-time-bus-query'); ?></p> </div> </div> 在`templates/route-planning.php`中创建路线规划表单: <div class="rtbq-container rtbq-route-container"> <div class="rtbq-header"> <h3><?php echo esc_html($atts['title']); ?></h3> </div> <div class="rtbq-route-form"> <div class="rtbq-input-group"> <label for="rtbq-route-city"><?php _e('城市', 'real-time-bus-query'); ?>:</label> <select id="rtbq-route-city" class="rtbq-city-select"> <option value="北京">北京</option> <option value="上海">上海</option> <option value="广州">广州</option> <option value="深圳">深圳</option> <option value="杭州">杭州</option> <option value="南京">南京</option> <option value="武汉">武汉</option> <option value="成都">成都</option> <option value="重庆">重庆</option> <option value="天津">天津</option> </select> </div> <div class="rtbq-input-group"> <label for="rtbq-origin-search"><?php _e('起点', 'real-time-bus-query'); ?>:</label> <div class="rtbq-search-wrapper"> <input type="text" id="rtbq-origin-search" class="rtbq-station-search" placeholder="<?php _e('输入起点位置', 'real-time-bus-query'); ?>" autocomplete="off"> <button type="button" class="rtbq-search-btn" data-target="origin"> <span class="dashicons dashicons-search"></span> </button> </div> <div id="rtbq-origin-results" class="rtbq-search-results"></div> </div> <div class="rtbq-input-group"> <label for="rtbq-destination-search"><?php _e('终点', 'real-time-bus-query'); ?>:</label> <div class="rtbq-search-wrapper"> <input type="text" id="rtbq-destination-search" class="rtbq-station-search" placeholder="<?php _e('输入终点位置', 'real-time-bus-query'); ?>" autocomplete="off"> <button type="button" class="rtbq-search-btn" data-target="destination"> <span class="dashicons dashicons-search"></span> </button> </div> <div id="rtbq-destination-results" class="rtbq-search-results"></div> </div> <div class="rtbq-route-options"> <label> <input type="checkbox" id="rtbq-avoid-congestion" checked> <?php _e('躲避拥堵', 'real-time-bus-query'); ?> </label> <label> <input type="checkbox" id="rtbq-walk-first" checked> <?php _e('步行优先', 'real-time-bus-query'); ?> </label> </div> <button type="button" id="rtbq-plan-route" class="rtbq-primary-btn"> <?php _e('开始规划', 'real-time-bus-query'); ?> </button> </div> <div class="rtbq-route-results" style="display: none;"> <div class="rtbq-route-summary"> <h4><?php _e('路线规划结果', 'real-time-bus-query'); ?></h4> <div id="rtbq-route-info" class="rtbq-route-info"> <!-- 路线摘要信息 --> </div> </div> <div class="rtbq-route-details"> <div id="rtbq-route-steps" class="rtbq-route-steps"> <!-- 详细路线步骤 --> </div> </div> <div class="rtbq-route-alternatives"> <h5><?php _e('备选方案', 'real-time-bus-query'); ?></h5> <div id="rtbq-alternative-routes" class="rtbq-alternative-routes"> <!-- 备选路线 --> </div> </div> </div> <div class="rtbq-loading" style="display: none;"> <div class="rtbq-spinner"></div> <p><?php _e('规划路线中...', 'real-time-bus-query'); ?></p> </div> </div> ### 3.3 创建前端JavaScript文件 在`assets/js/frontend.js`中创建前端交互逻辑: (function($) { 'use strict'; // 全局变量 var RTBQ = { currentStation: null, searchHistory: [], selectedOrigin: null, selectedDestination: null, // 初始化 init: function() { this.bindEvents(); this.loadSearchHistory(); this.setDefaultCity(); }, // 绑定事件 bindEvents: function() { // 站点搜索 $('.rtbq-station-search').on('input', this.handleSearchInput.bind(this)); $('.rtbq-search-btn').on('click', this.handleSearchClick.bind(this)); // 城市选择 $('.rtbq-city-select').on('change', this.handleCityChange.bind(this)); // 路线规划 $('#rtbq-plan-route').on('click', this.planRoute.bind(this)); // 点击外部关闭搜索结果 $(document).on('click', this.closeSearchResults.bind(this)); }, // 处理搜索输入 handleSearchInput: function(e) { var $input = $(e.target); var keyword = $input.val().trim(); if (keyword.length < 2) { this.hideSearchResults($input); return; } this.debouncedSearch(keyword, $input); }, // 防抖搜索 debouncedSearch: _.debounce(function(keyword, $input) { this.searchStations(keyword, $input); }, 300), // 搜索站点 searchStations: function(keyword, $input) { var city = $input.closest('.rtbq-container').find('.rtbq-city-select').val(); var target = $input.attr('id').includes('origin') ? 'origin' : $input.attr('id').includes('destination') ? 'destination' : 'station'; this.showLoading(true); $.ajax({ url: rtbq_ajax.rest_url + 'search-station', method: 'GET', data: { keyword: keyword, city: city }, success: function(response) { RTBQ.displaySearchResults(response, $input, target); }, error: function(xhr, status, error) { console.error('搜索失败:', error); RTBQ.showMessage('搜索失败,请重试', 'error'); }, complete: function() { RTBQ.showLoading(false); } }); }, // 显示搜索结果 displaySearchResults: function(results, $input, target) { var $resultsContainer = $input.siblings('.rtbq-search-results'); if (!results || results.length === 0) { $resultsContainer.html('<div class="rtbq-no-results">' + rtbq_ajax.strings.no_results + '</div>'); $resultsContainer.show(); return; } var html = '<ul class="rtbq-results-list">'; results.forEach(function(station) { html += '<li class="rtbq-result-item" data-station-id="' + station.id + '" data-station-name="' + station.name + '" data-station-address="' + station.address + '" data-location="' + station.location + '">'; html += '<strong>' + station.name + '</strong>'; html += '<br><small>' + station.address + '</small>'; html += '</li>'; }); html += '</ul>'; $resultsContainer.html(html); $resultsContainer.show(); // 绑定结果点击事件 $resultsContainer.find('.rtbq-result-item').on('click', function() { RTBQ.selectStation($(this), $input, target); }); }, // 选择站点 selectStation: function($item, $input, target) { var stationData = { id: $item.data('station-id'), name: $item.data('station-name'), address: $item.data('station-address'), location: $item.data('location') }; $input.val(stationData.name); this.hideSearchResults($input); if (target === 'station') { this.currentStation = stationData; this.loadBusLines(stationData.id); this.addToHistory(stationData); } else if (target === 'origin') { this.selectedOrigin = stationData; } else if (target === 'destination') { this.selectedDestination = stationData; } }, // 加载公交线路 loadBusLines: function(stationId) { var city = $('#rtbq-city-select').val(); this.showLoading(true); $('.rtbq-results-section').hide(); $.ajax({ url: rtbq_ajax.rest_url + 'bus-lines', method: 'GET', data: { station_id: stationId, city: city }, success: function(response) { RTBQ.displayBusLines(response); RTBQ.displayStationInfo(); $('.rtbq-results-section').show(); }, error: function(xhr, status, error) { console.error('加载公交线路失败:', error); RTBQ.showMessage('加载公交线路失败', 'error'); }, complete: function() { RTBQ.showLoading(false); } }); }, // 显示站点信息 displayStationInfo: function() { if (!this.currentStation) return; $('#rtbq-current-station').text(this.currentStation.name); $('#rtbq-station-address').text(this.currentStation.address); }, // 显示公交线路 displayBusLines: function(busLines) { var $busList = $('#rtbq-bus-list'); if (!busLines || busLines.length === 0) { $busList.html('<div class="rtbq-no-buses">暂无公交线路信息</div>'); return; } var html = '<div class="rtbq-bus-grid">'; busLines.forEach(function(bus) { html += '<div class="rtb

发表评论