跳至内容

分类: 网站建设

一步步教你,在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

发表评论

手把手教程,为WordPress实现基于用户行为的个性化弹窗与推荐工具

手把手教程:为WordPress实现基于用户行为的个性化弹窗与推荐工具 引言:个性化体验在网站运营中的重要性 在当今互联网环境中,用户期望获得与其兴趣和行为高度相关的个性化体验。根据Monetate的研究,93%的企业表示个性化策略显著提高了收入。对于WordPress网站而言,实现基于用户行为的个性化弹窗和推荐工具不仅能提升用户体验,还能有效提高转化率、延长页面停留时间并增强用户粘性。 本教程将详细指导您如何通过WordPress代码二次开发,实现基于用户行为的个性化弹窗与推荐工具。我们将从基础概念讲起,逐步深入到具体实现,最终打造一个完整的个性化推荐系统。 第一部分:个性化系统基础架构设计 1.1 理解用户行为数据收集 在开始开发之前,我们需要明确要收集哪些用户行为数据。常见的用户行为数据包括: 页面浏览历史 点击行为 停留时间 滚动深度 搜索关键词 购买/下载历史 设备类型和地理位置 1.2 系统架构设计 我们的个性化系统将包含以下核心组件: 数据收集模块:通过JavaScript和PHP收集用户行为数据 数据处理模块:分析用户行为并生成用户画像 存储模块:将用户数据存储在数据库中 推荐引擎:根据用户画像生成个性化内容 弹窗与推荐展示模块:在适当时机展示个性化内容 1.3 技术栈选择 前端:JavaScript (原生或jQuery)、CSS3、HTML5 后端:PHP (WordPress核心)、MySQL 数据存储:WordPress自定义表、Transients API、Cookies 推荐算法:基于内容的过滤、协同过滤简化版 第二部分:搭建用户行为追踪系统 2.1 创建数据库表存储用户行为 首先,我们需要创建一个自定义数据库表来存储用户行为数据。在您的主题的functions.php文件中添加以下代码: // 创建用户行为追踪表 function create_user_behavior_table() { global $wpdb; $table_name = $wpdb->prefix . 'user_behavior'; $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, user_id varchar(100) NOT NULL, session_id varchar(100) NOT NULL, behavior_type varchar(50) NOT NULL, behavior_value text NOT NULL, page_url varchar(500) NOT NULL, timestamp datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, PRIMARY KEY (id), KEY user_id (user_id), KEY behavior_type (behavior_type), KEY timestamp (timestamp) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } add_action('after_setup_theme', 'create_user_behavior_table'); 2.2 实现用户识别系统 为了追踪匿名用户和登录用户的行为,我们需要创建一个用户识别系统: // 生成或获取用户唯一标识 function get_user_identifier() { $identifier = ''; // 检查是否已登录 if (is_user_logged_in()) { $user_id = get_current_user_id(); $identifier = 'user_' . $user_id; } else { // 对于匿名用户,使用cookie存储的标识符 if (isset($_COOKIE['wp_visitor_id'])) { $identifier = 'visitor_' . $_COOKIE['wp_visitor_id']; } else { // 生成新的唯一标识符 $visitor_id = wp_generate_uuid4(); setcookie('wp_visitor_id', $visitor_id, time() + (365 * 24 * 60 * 60), '/'); $identifier = 'visitor_' . $visitor_id; } } return $identifier; } // 生成UUID4 function wp_generate_uuid4() { return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) ); } 2.3 前端行为追踪脚本 创建一个JavaScript文件来收集用户行为数据: // 文件路径: /wp-content/themes/your-theme/js/user-behavior-tracker.js (function() { 'use strict'; // 用户行为追踪对象 var UserBehaviorTracker = { // 配置 config: { endpoint: '/wp-admin/admin-ajax.php', trackingInterval: 30000, // 30秒发送一次数据 scrollThresholds: [25, 50, 75, 90] }, // 存储待发送的数据 pendingData: [], // 初始化 init: function() { this.bindEvents(); this.startPeriodicTracking(); this.trackPageView(); }, // 绑定事件 bindEvents: function() { // 点击事件 document.addEventListener('click', this.trackClick.bind(this)); // 滚动事件 var scrollTimeout; window.addEventListener('scroll', function() { clearTimeout(scrollTimeout); scrollTimeout = setTimeout(function() { UserBehaviorTracker.trackScroll(); }, 500); }); // 表单提交事件 document.addEventListener('submit', this.trackFormSubmit.bind(this)); // 页面离开事件 window.addEventListener('beforeunload', this.sendPendingData.bind(this, true)); }, // 追踪页面浏览 trackPageView: function() { var pageData = { behavior_type: 'page_view', behavior_value: JSON.stringify({ page_title: document.title, referrer: document.referrer || '', screen_resolution: window.screen.width + 'x' + window.screen.height, viewport_size: window.innerWidth + 'x' + window.innerHeight }), page_url: window.location.href }; this.addToPendingData(pageData); }, // 追踪点击事件 trackClick: function(event) { var target = event.target; var clickableElement = this.findClickableElement(target); if (clickableElement) { var clickData = { behavior_type: 'click', behavior_value: JSON.stringify({ element_type: clickableElement.tagName, element_id: clickableElement.id || '', element_class: clickableElement.className || '', element_text: this.getElementText(clickableElement).substring(0, 100), element_href: clickableElement.href || '' }), page_url: window.location.href }; this.addToPendingData(clickData); } }, // 追踪滚动深度 trackScroll: function() { var scrollTop = window.pageYOffset || document.documentElement.scrollTop; var scrollHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight; var scrollPercentage = scrollHeight > 0 ? Math.round((scrollTop / scrollHeight) * 100) : 0; // 检查是否达到阈值 for (var i = 0; i < this.config.scrollThresholds.length; i++) { var threshold = this.config.scrollThresholds[i]; if (scrollPercentage >= threshold && scrollPercentage < threshold + 5) { var scrollData = { behavior_type: 'scroll', behavior_value: JSON.stringify({ scroll_percentage: scrollPercentage, scroll_position: scrollTop, page_height: scrollHeight }), page_url: window.location.href }; this.addToPendingData(scrollData); break; } } }, // 追踪表单提交 trackFormSubmit: function(event) { var form = event.target; var formData = { behavior_type: 'form_submit', behavior_value: JSON.stringify({ form_id: form.id || '', form_class: form.className || '', form_action: form.action || '' }), page_url: window.location.href }; this.addToPendingData(formData); }, // 辅助方法:查找可点击元素 findClickableElement: function(element) { var clickableTags = ['A', 'BUTTON', 'INPUT', 'SELECT', 'TEXTAREA']; while (element && element !== document) { if (clickableTags.indexOf(element.tagName) !== -1) { return element; } element = element.parentNode; } return null; }, // 辅助方法:获取元素文本 getElementText: function(element) { return element.innerText || element.textContent || ''; }, // 添加到待发送数据 addToPendingData: function(data) { this.pendingData.push(data); // 如果数据量较大,立即发送 if (this.pendingData.length >= 10) { this.sendPendingData(); } }, // 发送待处理数据 sendPendingData: function(sync) { if (this.pendingData.length === 0) return; var dataToSend = this.pendingData.slice(); this.pendingData = []; var requestData = { action: 'save_user_behavior', behaviors: dataToSend }; if (sync) { // 同步发送(用于页面离开时) this.sendRequestSync(requestData); } else { // 异步发送 this.sendRequestAsync(requestData); } }, // 异步发送请求 sendRequestAsync: function(data) { var xhr = new XMLHttpRequest(); xhr.open('POST', this.config.endpoint, true); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if (xhr.status !== 200) { console.error('行为数据发送失败:', xhr.statusText); } } }; xhr.send(this.serializeData(data)); }, // 同步发送请求 sendRequestSync: function(data) { var xhr = new XMLHttpRequest(); xhr.open('POST', this.config.endpoint, false); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.send(this.serializeData(data)); }, // 序列化数据 serializeData: function(obj) { var str = []; for (var p in obj) { if (obj.hasOwnProperty(p)) { str.push(encodeURIComponent(p) + "=" + encodeURIComponent( typeof obj[p] === 'object' ? JSON.stringify(obj[p]) : obj[p] )); } } return str.join("&"); }, // 开始定期追踪 startPeriodicTracking: function() { setInterval(this.sendPendingData.bind(this), this.config.trackingInterval); } }; // 初始化追踪器 document.addEventListener('DOMContentLoaded', function() { UserBehaviorTracker.init(); }); // 暴露到全局作用域 window.UserBehaviorTracker = UserBehaviorTracker; })(); 2.4 后端数据处理接口 在functions.php中添加处理前端发送的行为数据的代码: // 保存用户行为数据 function save_user_behavior_callback() { // 验证nonce if (!check_ajax_referer('user_behavior_nonce', 'security', false)) { wp_die('安全验证失败', 403); } // 获取用户标识符 $user_identifier = get_user_identifier(); // 获取session ID $session_id = session_id(); if (empty($session_id)) { session_start(); $session_id = session_id(); } // 获取行为数据 $behaviors = json_decode(stripslashes($_POST['behaviors']), true); if (is_array($behaviors)) { global $wpdb; $table_name = $wpdb->prefix . 'user_behavior'; foreach ($behaviors as $behavior) { $wpdb->insert( $table_name, array( 'user_id' => $user_identifier, 'session_id' => $session_id, 'behavior_type' => sanitize_text_field($behavior['behavior_type']), 'behavior_value' => sanitize_text_field($behavior['behavior_value']), 'page_url' => esc_url_raw($behavior['page_url']) ), array('%s', '%s', '%s', '%s', '%s') ); } wp_send_json_success(array('message' => '行为数据保存成功', 'count' => count($behaviors))); } else { wp_send_json_error('无效的行为数据格式'); } } add_action('wp_ajax_save_user_behavior', 'save_user_behavior_callback'); add_action('wp_ajax_nopriv_save_user_behavior', 'save_user_behavior_callback'); // 注册并加载追踪脚本 function enqueue_user_behavior_tracker() { // 注册脚本 wp_register_script( 'user-behavior-tracker', get_template_directory_uri() . '/js/user-behavior-tracker.js', array(), '1.0.0', true ); // 本地化脚本,传递必要数据 wp_localize_script('user-behavior-tracker', 'userBehaviorTrackerVars', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('user_behavior_nonce') )); // 加载脚本 wp_enqueue_script('user-behavior-tracker'); } add_action('wp_enqueue_scripts', 'enqueue_user_behavior_tracker'); 第三部分:构建用户画像系统 3.1 分析用户行为数据 创建用户画像分析函数,定期分析用户行为并生成用户标签: // 分析用户行为并生成用户画像 function analyze_user_profile($user_identifier, $days = 30) { global $wpdb; $table_name = $wpdb->prefix . 'user_behavior'; // 获取用户最近的行为数据 $date_limit = date('Y-m-d H:i:s', strtotime("-$days days")); $behaviors = $wpdb->get_results($wpdb->prepare( "SELECT behavior_type, behavior_value, page_url, timestamp FROM $table_name WHERE user_id = %s AND timestamp >= %s ORDER BY timestamp DESC", $user_identifier, $date_limit )); if (empty($behaviors)) { return false; } $profile = array( 'interests' => array(), 'behavior_patterns' => array(), 'preferred_categories' => array(), 'preferred_tags' => array(), 'engagement_level' => 'low', 'last_activity' => $behaviors[0]->timestamp, 'total_visits' => 0, 'avg_session_duration' => 0 ); // 分析页面浏览和兴趣 $page_views = array(); $category_views = array(); $tag_views = array(); foreach ($behaviors as $behavior) { if ($behavior->behavior_type === 'page_view') { $profile['total_visits']++; // 解析页面URL,获取分类和标签信息 $post_id = url_to_postid($behavior->page_url); if ($post_id) { // 获取分类 $categories = wp_get_post_categories($post_id); foreach ($categories as $cat_id) { $category = get_category($cat_id); if ($category) { if (!isset($category_views[$category->slug])) { $category_views[$category->slug] = 0; } $category_views[$category->slug]++; } } // 获取标签 $tags = wp_get_post_tags($post_id); foreach ($tags as $tag) { if (!isset($tag_views[$tag->slug])) { $tag_views[$tag->slug] = 0; } $tag_views[$tag->slug]++; } } } } // 确定最感兴趣的分类 arsort($category_views); $profile['preferred_categories'] = array_slice(array_keys($category_views), 0, 5); // 确定最感兴趣的标签 arsort($tag_views); $profile['preferred_tags'] = array_slice(array_keys($tag_views), 0, 10); // 分析参与度 $total_duration = 0; $session_count = 0; $current_session = array(); $last_timestamp = null; foreach ($behaviors as $index => $behavior) { if ($last_timestamp) { $time_diff = strtotime($last_timestamp) - strtotime($behavior->timestamp); // 如果时间差小于30分钟,视为同一会话 if ($time_diff < 1800) { if (empty($current_session)) { $current_session[] = $last_timestamp; } $current_session[] = $behavior->timestamp; } else { // 会话结束,计算持续时间 if (!empty($current_session)) { $session_start = strtotime(end($current_session)); $session_end = strtotime($current_session[0]); $total_duration += $session_duration; $session_count++; $current_session = array(); } } } $last_timestamp = $behavior->timestamp; } // 处理最后一个会话 if (!empty($current_session)) { $session_start = strtotime(end($current_session)); $session_end = strtotime($current_session[0]); $session_duration = $session_end - $session_start; $total_duration += $session_duration; $session_count++; } // 计算平均会话时长 if ($session_count > 0) { $profile['avg_session_duration'] = round($total_duration / $session_count); // 确定参与度等级 if ($profile['avg_session_duration'] > 600) { // 10分钟以上 $profile['engagement_level'] = 'high'; } elseif ($profile['avg_session_duration'] > 180) { // 3-10分钟 $profile['engagement_level'] = 'medium'; } } // 分析点击行为模式 $click_patterns = array(); foreach ($behaviors as $behavior) { if ($behavior->behavior_type === 'click') { $click_data = json_decode($behavior->behavior_value, true); if ($click_data && isset($click_data['element_type'])) { $element_type = $click_data['element_type']; if (!isset($click_patterns[$element_type])) { $click_patterns[$element_type] = 0; } $click_patterns[$element_type]++; } } } $profile['behavior_patterns'] = $click_patterns; // 分析滚动深度 $scroll_data = array(); foreach ($behaviors as $behavior) { if ($behavior->behavior_type === 'scroll') { $scroll_info = json_decode($behavior->behavior_value, true); if ($scroll_info && isset($scroll_info['scroll_percentage'])) { $scroll_data[] = $scroll_info['scroll_percentage']; } } } if (!empty($scroll_data)) { $profile['avg_scroll_depth'] = round(array_sum($scroll_data) / count($scroll_data)); } return $profile; } // 获取或生成用户画像 function get_user_profile($user_identifier, $force_refresh = false) { $transient_key = 'user_profile_' . md5($user_identifier); // 如果不需要强制刷新且存在缓存的画像,直接返回 if (!$force_refresh) { $cached_profile = get_transient($transient_key); if ($cached_profile !== false) { return $cached_profile; } } // 分析用户行为生成新画像 $profile = analyze_user_profile($user_identifier); if ($profile) { // 缓存用户画像12小时 set_transient($transient_key, $profile, 12 * HOUR_IN_SECONDS); // 同时存储到用户元数据(如果用户已登录) if (strpos($user_identifier, 'user_') === 0) { $user_id = str_replace('user_', '', $user_identifier); update_user_meta($user_id, 'personalization_profile', $profile); } } return $profile; } 3.2 创建用户画像管理界面 为了方便查看和管理用户画像,我们可以创建一个简单的管理界面: // 添加用户画像管理菜单 function add_user_profiles_admin_menu() { add_menu_page( '用户画像分析', '用户画像', 'manage_options', 'user-profiles', 'display_user_profiles_page', 'dashicons-admin-users', 30 ); } add_action('admin_menu', 'add_user_profiles_admin_menu'); // 显示用户画像页面 function display_user_profiles_page() { global $wpdb; $table_name = $wpdb->prefix . 'user_behavior'; // 获取所有用户标识符 $user_identifiers = $wpdb->get_col("SELECT DISTINCT user_id FROM $table_name ORDER BY user_id"); echo '<div class="wrap">'; echo '<h1>用户画像分析</h1>'; if (empty($user_identifiers)) { echo '<p>暂无用户行为数据。</p>'; return; } echo '<table class="wp-list-table widefat fixed striped">'; echo '<thead><tr> <th>用户ID</th> <th>最后活动</th> <th>访问次数</th> <th>兴趣分类</th> <th>兴趣标签</th> <th>参与度</th> <th>操作</th> </tr></thead>'; echo '<tbody>'; foreach ($user_identifiers as $identifier) { $profile = get_user_profile($identifier); if (!$profile) { continue; } echo '<tr>'; echo '<td>' . esc_html($identifier) . '</td>'; echo '<td>' . esc_html($profile['last_activity']) . '</td>'; echo '<td>' . esc_html($profile['total_visits']) . '</td>'; echo '<td>' . esc_html(implode(', ', $profile['preferred_categories'])) . '</td>'; echo '<td>' . esc_html(implode(', ', array_slice($profile['preferred_tags'], 0, 3))) . '...</td>'; echo '<td>' . esc_html($profile['engagement_level']) . ' (' . esc_html($profile['avg_session_duration']) . '秒)</td>'; echo '<td><button class="button button-small view-profile-details" data-user="' . esc_attr($identifier) . '">查看详情</button></td>'; echo '</tr>'; } echo '</tbody></table>'; // 添加详情模态框 echo '<div id="profile-details-modal" style="display:none;"> <div id="profile-details-content"></div> </div>'; // 添加JavaScript echo '<script> jQuery(document).ready(function($) { $(".view-profile-details").click(function() { var userIdentifier = $(this).data("user"); $.ajax({ url: ajaxurl, type: "POST", data: { action: "get_user_profile_details", user_identifier: userIdentifier }, success: function(response) { if (response.success) { $("#profile-details-content").html(response.data); $("#profile-details-modal").dialog({ modal: true, width: 800, height: 600, title: "用户画像详情 - " + userIdentifier }); } } }); }); }); </script>'; echo '</div>'; } // AJAX获取用户画像详情 function get_user_profile_details_callback() { $user_identifier = sanitize_text_field($_POST['user_identifier']); $profile = get_user_profile($user_identifier, true); if (!$profile) { wp_send_json_error('未找到用户画像数据'); } $html = '<div class="user-profile-details">'; $html .= '<h3>基本信息</h3>'; $html .= '<p><strong>最后活动:</strong> ' . esc_html($profile['last_activity']) . '</p>'; $html .= '<p><strong>总访问次数:</strong> ' . esc_html($profile['total_visits']) . '</p>'; $html .= '<p><strong>平均会话时长:</strong> ' . esc_html($profile['avg_session_duration']) . ' 秒</p>'; $html .= '<p><strong>参与度等级:</strong> ' . esc_html($profile['engagement_level']) . '</p>'; if (isset($profile['avg_scroll_depth'])) { $html .= '<p><strong>平均滚动深度:</strong> ' . esc_html($profile['avg_scroll_depth']) . '%</p>'; } $html .= '<h3>兴趣分类</h3>'; $html .= '<ul>'; foreach ($profile['preferred_categories'] as $category) { $html .= '<li>' . esc_html($category) . '</li>'; } $html .= '</ul>'; $html .= '<h3>兴趣标签</h3>'; $html .= '<ul>'; foreach ($profile['preferred_tags'] as $tag) { $html .= '<li>' . esc_html($tag) . '</li>'; } $html .= '</ul>'; $html .= '<h3>行为模式</h3>'; if (!empty($profile['behavior_patterns'])) { $html .= '<ul>'; foreach ($profile['behavior_patterns'] as $element_type => $count) { $html .= '<li><strong>' . esc_html($element_type) . ':</strong> ' . esc_html($count) . ' 次点击</li>'; } $html .= '</ul>'; } else { $html .= '<p>暂无行为模式数据</p>'; } $html .= '</div>'; wp_send_json_success($html); } add_action('wp_ajax_get_user_profile_details', 'get_user_profile_details_callback'); 第四部分:实现个性化推荐引擎 4.1 基于内容的推荐算法 // 获取个性化推荐内容 function get_personalized_recommendations($user_identifier, $limit = 5) { $profile = get_user_profile($user_identifier); if (!$profile || empty($profile['preferred_categories']) || empty($profile['preferred_tags'])) { // 如果没有用户画像,返回热门内容 return get_popular_posts($limit); } // 构建推荐查询 $args = array( 'post_type' => 'post', 'posts_per_page' => $limit, 'post_status' => 'publish', 'orderby' => 'relevance', 'meta_query' => array( 'relation' => 'OR' ), 'tax_query' => array( 'relation' => 'OR' ) ); // 添加分类查询 if (!empty($profile['preferred_categories'])) { $args['tax_query'][] = array( 'taxonomy' => 'category', 'field' => 'slug', 'terms' => $profile['preferred_categories'], 'operator' => 'IN' ); } // 添加标签查询 if (!empty($profile['preferred_tags'])) { $args['tax_query'][] = array( 'taxonomy' => 'post_tag', 'field' => 'slug', 'terms' => $profile['preferred_tags'], 'operator' => 'IN' ); } // 排除用户已经看过的文章 $viewed_posts = get_user_viewed_posts($user_identifier); if (!empty($viewed_posts)) { $args['post__not_in'] = $viewed_posts; } // 根据参与度调整时间范围 if ($profile['engagement_level'] === 'high') { // 高参与度用户:显示更广泛的内容 $args['date_query'] = array( array( 'after' => '3 months ago' ) ); } elseif ($profile['engagement_level'] === 'medium') { // 中等参与度用户:显示近期热门内容 $args['date_query'] = array( array( 'after' => '1 month ago' ) ); $args['orderby'] = 'comment_count'; } else { // 低参与度用户:显示最新内容 $args['orderby'] = 'date'; $args['order'] = 'DESC'; } // 执行查询 $recommendations_query = new WP_Query($args); if ($recommendations_query->have_posts()) { return $recommendations_query->posts; } else { // 如果查询无结果,返回备用推荐 return get_fallback_recommendations($limit); } } // 获取用户已浏览的文章 function get_user_viewed_posts($user_identifier, $limit = 50) { global $wpdb; $table_name = $wpdb->prefix . 'user_behavior'; $viewed_urls = $wpdb->get_col($wpdb->prepare( "SELECT DISTINCT page_url FROM $table_name WHERE user_id = %s AND behavior_type = 'page_view' ORDER BY timestamp DESC LIMIT %d", $user_identifier, $limit )); $viewed_posts = array(); foreach ($viewed_urls as $url) { $post_id = url_to_postid($url); if ($post_id) { $viewed_posts[] = $post_id; } } return array_unique($viewed_posts); } // 获取热门文章(备用推荐) function get_popular_posts($limit = 5) { $args = array( 'post_type' => 'post', 'posts_per_page' => $limit, 'post_status' => 'publish', 'orderby' => 'comment_count', 'order' => 'DESC', 'date_query' => array( array( 'after' => '1 month ago' ) ) ); $query = new WP_Query($args); return $query->posts; } // 获取备用推荐(当个性化推荐不足时) function get_fallback_recommendations($limit = 5) { // 策略1:获取编辑推荐的文章 $editor_picks = get_posts(array( 'post_type' => 'post', 'posts_per_page' => $limit, 'post_status' => 'publish', 'meta_key' => 'editor_pick', 'meta_value' => '1', 'orderby' => 'date', 'order' => 'DESC' )); if (count($editor_picks) >= $limit) { return $editor_picks; } // 策略2:获取最新文章补充 $latest_posts = get_posts(array( 'post_type' => 'post', 'posts_per_page' => $limit - count($editor_picks), 'post_status' => 'publish', 'orderby' => 'date', 'order' => 'DESC', 'post__not_in' => wp_list_pluck($editor_picks, 'ID') )); return array_merge($editor_picks, $latest_posts); } 4.2 实时推荐API 创建实时推荐API,供前端动态获取推荐内容: // 实时推荐API function get_realtime_recommendations_callback() { // 验证nonce if (!check_ajax_referer('recommendations_nonce', 'security', false)) { wp_send_json_error('安全验证失败'); } $user_identifier = get_user_identifier(); $limit = isset($_POST['limit']) ? intval($_POST['limit']) : 5; $context = isset($_POST['context']) ? sanitize_text_field($_POST['context']) : 'general'; // 根据上下文调整推荐策略 $recommendations = get_personalized_recommendations($user_identifier, $limit); // 格式化推荐结果 $formatted_recommendations = array(); foreach ($recommendations as $post) { $formatted_recommendations[] = array( 'id' => $post->ID, 'title' => get_the_title($post), 'excerpt' => wp_trim_words(get_the_excerpt($post), 20), 'url' => get_permalink($post), 'image' => get_the_post_thumbnail_url($post, 'medium'), 'date' => get_the_date('', $post), 'categories' => wp_get_post_categories($post->ID, array('fields' => 'names')) ); } // 添加推荐原因 $profile = get_user_profile($user_identifier); $reason = '根据您的浏览历史为您推荐'; if ($profile && !empty($profile['preferred_categories'])) { $reason = '基于您对' . implode('、', array_slice($profile['preferred_categories'], 0, 2)) . '的兴趣推荐'; } wp_send_json_success(array( 'recommendations' => $formatted_recommendations, 'reason' => $reason, 'context' => $context )); } add_action('wp_ajax_get_realtime_recommendations', 'get_realtime_recommendations_callback'); add_action('wp_ajax_nopriv_get_realtime_recommendations', 'get_realtime_recommendations_callback'); 第五部分:实现个性化弹窗系统 5.1 弹窗触发条件与规则引擎 // 弹窗规则引擎 class Personalized_Popup_Rules { private $user_identifier; private $user_profile; private $current_page; public function __construct() { $this->user_identifier = get_user_identifier(); $this->user_profile = get_user_profile($this->user_identifier); $this->current_page = $this->get_current_page_info(); } // 获取当前页面信息 private function get_current_page_info() { global $post; $page_info = array( 'id' => get_the_ID(), 'type' => get_post_type(), 'url' => get_permalink(), 'categories' => array(), 'tags' => array() ); if ($post) { $page_info['categories'] = wp_get_post_categories($post->ID, array('fields' => 'slugs')); $page_info['tags'] = wp_get_post_tags($post->ID, array('fields' => 'slugs')); } return $page_info; }

发表评论

开发指南,打造网站内嵌的在线简易图片编辑与美化工具

开发指南:打造网站内嵌的在线简易图片编辑与美化工具 引言:为什么网站需要内置图片编辑工具? 在当今视觉内容主导的互联网环境中,图片已成为网站内容不可或缺的组成部分。无论是博客文章、产品展示还是社交媒体分享,高质量的图片都能显著提升用户体验和内容吸引力。然而,许多网站运营者面临一个共同挑战:用户上传的图片往往需要调整大小、裁剪、添加水印或进行简单美化,而传统的解决方案要么功能过于复杂,要么需要跳转到外部工具,导致用户体验中断。 WordPress作为全球最流行的内容管理系统,拥有强大的扩展能力。通过代码二次开发,我们可以在WordPress网站中嵌入一个轻量级的在线图片编辑与美化工具,让用户在不离开网站的情况下完成基本的图片处理操作。这不仅提升了用户体验,还能增加用户粘性和内容生产效率。 本文将详细介绍如何通过WordPress代码二次开发,实现一个功能完善但操作简单的内嵌图片编辑工具,涵盖从需求分析、技术选型到具体实现的完整流程。 一、需求分析与功能规划 1.1 核心功能需求 在开始开发之前,我们需要明确工具应具备的核心功能: 基础编辑功能: 图片裁剪与调整大小 旋转与翻转 亮度、对比度、饱和度调整 锐化与模糊效果 美化增强功能: 滤镜与特效应用 文字添加与样式设置 贴图与形状叠加 边框与阴影效果 实用工具: 图片压缩与格式转换 水印添加与管理 批量处理基础功能 撤销/重做操作 用户体验需求: 响应式设计,适配不同设备 直观的操作界面 实时预览效果 快速导出与保存 1.2 技术可行性分析 基于WordPress平台,我们有多种技术方案可选: 前端技术栈: HTML5 Canvas:用于图片处理的核心技术 JavaScript(ES6+):实现交互逻辑 CSS3:界面样式与动画效果 图片处理库选择: Fabric.js:功能强大的Canvas库,适合交互式图片编辑 Caman.js:专注于滤镜和颜色调整 原生Canvas API:更轻量,但开发复杂度较高 WordPress集成方案: 短代码(Shortcode)嵌入 Gutenberg块编辑器集成 独立管理页面 媒体库扩展 综合考虑开发效率与功能需求,我们选择Fabric.js作为核心图片处理库,通过短代码和媒体库扩展的方式集成到WordPress中。 二、开发环境搭建与准备工作 2.1 开发环境配置 本地WordPress环境: 安装Local by Flywheel或XAMPP 配置PHP 7.4+环境 确保启用GD库和ImageMagick扩展 代码编辑器准备: VS Code或PHPStorm 安装WordPress开发相关插件 版本控制: 初始化Git仓库 建立合理的分支管理策略 2.2 WordPress插件基础结构 创建插件目录结构: wp-image-editor-tool/ ├── wp-image-editor-tool.php # 主插件文件 ├── includes/ # 核心功能文件 │ ├── class-editor-core.php # 编辑器核心类 │ ├── class-image-processor.php # 图片处理类 │ └── class-ajax-handler.php # AJAX处理类 ├── admin/ # 后台管理文件 │ ├── css/ │ ├── js/ │ └── class-admin-settings.php ├── public/ # 前端文件 │ ├── css/ │ ├── js/ │ └── templates/ ├── assets/ # 静态资源 │ ├── fonts/ │ ├── icons/ │ └── images/ └── vendor/ # 第三方库 └── fabric.js/ 2.3 插件主文件配置 <?php /** * Plugin Name: WordPress图片编辑工具 * Plugin URI: https://yourwebsite.com/ * Description: 在WordPress网站中嵌入在线图片编辑与美化工具 * Version: 1.0.0 * Author: 你的名字 * License: GPL v2 or later * Text Domain: wp-image-editor */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('WPIET_VERSION', '1.0.0'); define('WPIET_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('WPIET_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 require_once WPIET_PLUGIN_DIR . 'includes/class-plugin-init.php'; 三、核心编辑器实现 3.1 Canvas编辑器初始化 创建编辑器核心类,初始化Fabric.js画布: // public/js/editor-core.js class ImageEditor { constructor(canvasId, options = {}) { this.canvas = new fabric.Canvas(canvasId, { backgroundColor: '#f5f5f5', preserveObjectStacking: true, ...options }); this.history = []; this.historyIndex = -1; this.currentImage = null; this.initEvents(); } initEvents() { // 保存操作历史 this.canvas.on('object:modified', () => this.saveState()); this.canvas.on('object:added', () => this.saveState()); this.canvas.on('object:removed', () => this.saveState()); } saveState() { // 限制历史记录数量 if (this.history.length > 20) { this.history.shift(); } this.history.push(JSON.stringify(this.canvas.toJSON())); this.historyIndex = this.history.length - 1; } loadImage(url) { return new Promise((resolve, reject) => { fabric.Image.fromURL(url, (img) => { this.canvas.clear(); this.canvas.setBackgroundImage(img, this.canvas.renderAll.bind(this.canvas), { scaleX: this.canvas.width / img.width, scaleY: this.canvas.height / img.height }); this.currentImage = img; this.saveState(); resolve(img); }, { crossOrigin: 'anonymous' }); }); } } 3.2 基础编辑功能实现 3.2.1 裁剪功能 class CropTool { constructor(editor) { this.editor = editor; this.isCropping = false; this.cropRect = null; } startCrop() { this.isCropping = true; this.cropRect = new fabric.Rect({ left: 100, top: 100, width: 200, height: 200, fill: 'rgba(0,0,0,0.3)', stroke: '#ffffff', strokeWidth: 2, strokeDashArray: [5, 5], selectable: true, hasControls: true, hasBorders: true }); this.editor.canvas.add(this.cropRect); this.editor.canvas.setActiveObject(this.cropRect); } applyCrop() { if (!this.cropRect || !this.editor.currentImage) return; const canvas = this.editor.canvas; const rect = this.cropRect; // 计算裁剪区域 const scaleX = this.editor.currentImage.scaleX; const scaleY = this.editor.currentImage.scaleY; const cropData = { left: (rect.left - this.editor.currentImage.left) / scaleX, top: (rect.top - this.editor.currentImage.top) / scaleY, width: rect.width / scaleX, height: rect.height / scaleY }; // 创建临时canvas进行裁剪 const tempCanvas = document.createElement('canvas'); const tempCtx = tempCanvas.getContext('2d'); tempCanvas.width = cropData.width; tempCanvas.height = cropData.height; tempCtx.drawImage( canvas.lowerCanvasEl, cropData.left, cropData.top, cropData.width, cropData.height, 0, 0, cropData.width, cropData.height ); // 加载裁剪后的图片 fabric.Image.fromURL(tempCanvas.toDataURL(), (img) => { canvas.clear(); canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas)); this.editor.currentImage = img; this.editor.saveState(); this.isCropping = false; this.cropRect = null; }); } } 3.2.2 调整工具 class AdjustmentTool { constructor(editor) { this.editor = editor; } adjustBrightness(value) { const filters = this.editor.currentImage.filters || []; const brightnessFilter = new fabric.Image.filters.Brightness({ brightness: value / 100 }); // 查找并更新或添加亮度滤镜 const existingIndex = filters.findIndex(f => f.type === 'Brightness'); if (existingIndex >= 0) { filters[existingIndex] = brightnessFilter; } else { filters.push(brightnessFilter); } this.applyFilters(filters); } adjustContrast(value) { const filters = this.editor.currentImage.filters || []; const contrastFilter = new fabric.Image.filters.Contrast({ contrast: value / 100 }); const existingIndex = filters.findIndex(f => f.type === 'Contrast'); if (existingIndex >= 0) { filters[existingIndex] = contrastFilter; } else { filters.push(contrastFilter); } this.applyFilters(filters); } applyFilters(filters) { this.editor.currentImage.filters = filters; this.editor.currentImage.applyFilters(); this.editor.canvas.renderAll(); this.editor.saveState(); } } 3.3 美化功能实现 3.3.1 滤镜系统 class FilterSystem { constructor(editor) { this.editor = editor; this.presets = { vintage: { brightness: -0.05, saturation: 0.1, sepia: 0.3 }, blackWhite: { saturation: -1 }, cool: { brightness: 0.05, saturation: 0.2, tint: { color: '#0099ff', opacity: 0.1 } }, warm: { brightness: 0.05, saturation: 0.1, tint: { color: '#ff9900', opacity: 0.1 } } }; } applyPreset(presetName) { const preset = this.presets[presetName]; if (!preset) return; const filters = []; if (preset.brightness !== undefined) { filters.push(new fabric.Image.filters.Brightness({ brightness: preset.brightness })); } if (preset.saturation !== undefined) { filters.push(new fabric.Image.filters.Saturation({ saturation: preset.saturation })); } if (preset.sepia !== undefined) { filters.push(new fabric.Image.filters.Sepia({ amount: preset.sepia })); } if (preset.tint) { filters.push(new fabric.Image.filters.BlendColor({ color: preset.tint.color, mode: 'tint', alpha: preset.tint.opacity })); } this.editor.currentImage.filters = filters; this.editor.currentImage.applyFilters(); this.editor.canvas.renderAll(); this.editor.saveState(); } applyCustomFilter(filterConfig) { // 实现自定义滤镜组合 const filters = []; Object.keys(filterConfig).forEach(key => { switch(key) { case 'brightness': filters.push(new fabric.Image.filters.Brightness(filterConfig[key])); break; case 'contrast': filters.push(new fabric.Image.filters.Contrast(filterConfig[key])); break; case 'saturation': filters.push(new fabric.Image.filters.Saturation(filterConfig[key])); break; // 更多滤镜类型... } }); this.editor.currentImage.filters = filters; this.editor.currentImage.applyFilters(); this.editor.canvas.renderAll(); this.editor.saveState(); } } 3.3.2 文字添加功能 class TextTool { constructor(editor) { this.editor = editor; this.defaultStyles = { fontSize: 24, fontFamily: 'Arial', fill: '#000000', fontWeight: 'normal', textAlign: 'left' }; } addText(content, options = {}) { const textOptions = { ...this.defaultStyles, ...options, left: this.editor.canvas.width / 2, top: this.editor.canvas.height / 2, editable: true }; const text = new fabric.Textbox(content, textOptions); text.setControlsVisibility({ mt: false, // 隐藏上中控制点 mb: false // 隐藏下中控制点 }); this.editor.canvas.add(text); this.editor.canvas.setActiveObject(text); this.editor.saveState(); return text; } updateTextStyle(textObject, styles) { textObject.set(styles); this.editor.canvas.renderAll(); this.editor.saveState(); } addTextShadow(textObject, shadowConfig) { textObject.set({ shadow: new fabric.Shadow({ color: shadowConfig.color || 'rgba(0,0,0,0.5)', blur: shadowConfig.blur || 5, offsetX: shadowConfig.offsetX || 2, offsetY: shadowConfig.offsetY || 2 }) }); this.editor.canvas.renderAll(); this.editor.saveState(); } } 四、WordPress集成与后端处理 4.1 短代码集成 创建短代码,让用户可以在文章或页面中嵌入图片编辑器: // includes/class-shortcode-handler.php class WPIET_Shortcode_Handler { public static function init() { add_shortcode('wp_image_editor', [__CLASS__, 'render_editor']); } public static function render_editor($atts) { $atts = shortcode_atts([ 'width' => '800', 'height' => '600', 'image_id' => '', 'toolbar' => 'basic' ], $atts); // 加载必要资源 wp_enqueue_style('wpiet-editor-style'); wp_enqueue_script('fabric-js'); wp_enqueue_script('wpiet-editor-script'); // 获取图片URL $image_url = ''; if (!empty($atts['image_id'])) { $image_url = wp_get_attachment_url($atts['image_id']); } // 渲染编辑器HTML ob_start(); ?> <div class="wpiet-editor-container" data-config="<?php echo esc_attr(json_encode($atts)); ?>"> <div class="wpiet-toolbar"> <!-- 工具栏内容 --> </div> <div class="wpiet-canvas-container"> <canvas id="wpiet-canvas" width="<?php echo esc_attr($atts['width']); ?>" height="<?php echo esc_attr($atts['height']); ?>"> </canvas> </div> <div class="wpiet-sidebar"> <!-- 侧边栏工具 --> </div> <div class="wpiet-controls"> <button class="wpiet-btn-save">保存图片</button> <button class="wpiet-btn-reset">重置</button> </div> </div> <?php return ob_get_clean(); } } 4.2 媒体库集成 扩展WordPress媒体库,添加"编辑图片"选项: // includes/class-media-library-integration.php class WPIET_Media_Library_Integration { public static function init() { // 在媒体库列表添加编辑链接 add_filter('media_row_actions', [__CLASS__, 'add_edit_action'], 10, 2); // 在附件详情页添加编辑按钮 add_action('attachment_submitbox_misc_actions', [__CLASS__, 'add_edit_button']); // 添加媒体库模态框中的编辑选项 add_action('print_media_templates', [__CLASS__, 'add_media_template']); } public static function add_edit_action($actions, $post) { if (wp_attachment_is_image($post)) { $edit_url = admin_url('admin.php?page=wpiet-edit&image_id=' . $post->ID); $actions['wpiet_edit'] = sprintf( '<a href="%s" target="_blank">%s</a>', esc_url($edit_url), __('编辑图片', 'wp-image-editor') ); } return $actions; } public static function add_edit_button() { global $post; if (!wp_attachment_is_image($post->ID)) { return; } ?> <div class="misc-pub-section"> <a href="<?php echo admin_url('admin.php?page=wpiet-edit&image_id=' . $post->ID); ?>" class="button button-large" target="_blank"> 开发指南:打造网站内嵌的在线简易图片编辑与美化工具(续) 4.3 AJAX图片处理与保存 4.3.1 后端图片处理类 // includes/class-image-processor.php class WPIET_Image_Processor { private $allowed_mime_types = [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ]; private $max_file_size = 5242880; // 5MB public function process_image($image_data, $operations = []) { // 验证图片数据 if (!$this->validate_image_data($image_data)) { return new WP_Error('invalid_image', '无效的图片数据'); } // 创建临时文件 $temp_file = $this->create_temp_file($image_data); if (is_wp_error($temp_file)) { return $temp_file; } // 应用图片操作 $processed_image = $this->apply_operations($temp_file, $operations); // 清理临时文件 unlink($temp_file); return $processed_image; } private function validate_image_data($image_data) { // 检查数据格式 if (!is_string($image_data) || empty($image_data)) { return false; } // 检查是否为有效的base64或URL if (strpos($image_data, 'data:image') === 0) { // base64格式 $parts = explode(',', $image_data); if (count($parts) !== 2) { return false; } // 解码并验证 $decoded = base64_decode($parts[1]); if ($decoded === false) { return false; } // 检查文件大小 if (strlen($decoded) > $this->max_file_size) { return false; } // 检查MIME类型 $finfo = finfo_open(FILEINFO_MIME_TYPE); $mime_type = finfo_buffer($finfo, $decoded); finfo_close($finfo); if (!in_array($mime_type, $this->allowed_mime_types)) { return false; } } return true; } private function create_temp_file($image_data) { $temp_dir = get_temp_dir(); $temp_file = tempnam($temp_dir, 'wpiet_'); if (strpos($image_data, 'data:image') === 0) { // base64数据 $parts = explode(',', $image_data); $image_binary = base64_decode($parts[1]); file_put_contents($temp_file, $image_binary); } else { // URL或文件路径 $response = wp_remote_get($image_data); if (is_wp_error($response)) { return $response; } $image_binary = wp_remote_retrieve_body($response); file_put_contents($temp_file, $image_binary); } return $temp_file; } private function apply_operations($image_path, $operations) { $editor = wp_get_image_editor($image_path); if (is_wp_error($editor)) { return $editor; } // 应用各项操作 foreach ($operations as $operation) { switch ($operation['type']) { case 'crop': $editor->crop( $operation['x'], $operation['y'], $operation['width'], $operation['height'] ); break; case 'resize': $editor->resize( $operation['width'], $operation['height'], $operation['crop'] ?? false ); break; case 'rotate': $editor->rotate($operation['angle']); break; case 'flip': $editor->flip( $operation['direction'] === 'horizontal' ? 'horiz' : 'vert' ); break; case 'filter': $this->apply_filter($editor, $operation); break; } } // 生成新文件名 $filename = 'edited-' . time() . '-' . wp_basename($image_path); $upload_dir = wp_upload_dir(); $file_path = $upload_dir['path'] . '/' . $filename; // 保存图片 $result = $editor->save($file_path); if (is_wp_error($result)) { return $result; } return [ 'path' => $result['path'], 'url' => $upload_dir['url'] . '/' . $result['file'], 'width' => $result['width'], 'height' => $result['height'], 'size' => filesize($result['path']) ]; } private function apply_filter($editor, $filter) { // 使用ImageMagick或GD应用滤镜 $image_path = $editor->get_file(); if (extension_loaded('imagick')) { $this->apply_imagick_filter($image_path, $filter); } else { $this->apply_gd_filter($image_path, $filter); } } private function apply_imagick_filter($image_path, $filter) { $imagick = new Imagick($image_path); switch ($filter['name']) { case 'brightness': $imagick->modulateImage( $filter['value'] + 100, 100, 100 ); break; case 'contrast': $imagick->sigmoidalContrastImage( true, $filter['value'] / 10, 0 ); break; case 'saturation': $imagick->modulateImage( 100, $filter['value'] + 100, 100 ); break; case 'sepia': $imagick->sepiaToneImage($filter['value']); break; case 'blur': $imagick->gaussianBlurImage( $filter['radius'], $filter['sigma'] ); break; } $imagick->writeImage($image_path); $imagick->destroy(); } } 4.3.2 AJAX处理器 // includes/class-ajax-handler.php class WPIET_Ajax_Handler { public static function init() { // 保存图片 add_action('wp_ajax_wpiet_save_image', [__CLASS__, 'save_image']); add_action('wp_ajax_nopriv_wpiet_save_image', [__CLASS__, 'save_image_nopriv']); // 获取图片信息 add_action('wp_ajax_wpiet_get_image_info', [__CLASS__, 'get_image_info']); // 批量处理 add_action('wp_ajax_wpiet_batch_process', [__CLASS__, 'batch_process']); } public static function save_image() { // 验证nonce if (!check_ajax_referer('wpiet_editor_nonce', 'nonce', false)) { wp_die('安全验证失败', 403); } // 验证权限 if (!current_user_can('upload_files')) { wp_die('权限不足', 403); } // 获取数据 $image_data = isset($_POST['image_data']) ? $_POST['image_data'] : ''; $operations = isset($_POST['operations']) ? json_decode(stripslashes($_POST['operations']), true) : []; $filename = isset($_POST['filename']) ? sanitize_file_name($_POST['filename']) : ''; if (empty($image_data)) { wp_send_json_error('没有图片数据'); } // 处理图片 $processor = new WPIET_Image_Processor(); $result = $processor->process_image($image_data, $operations); if (is_wp_error($result)) { wp_send_json_error($result->get_error_message()); } // 创建媒体库附件 $attachment_id = self::create_attachment($result['path'], $filename); if (is_wp_error($attachment_id)) { wp_send_json_error($attachment_id->get_error_message()); } // 返回结果 wp_send_json_success([ 'attachment_id' => $attachment_id, 'url' => $result['url'], 'edit_url' => get_edit_post_link($attachment_id), 'size' => size_format($result['size']) ]); } public static function save_image_nopriv() { // 非登录用户处理 if (!get_option('wpiet_allow_guest_upload', false)) { wp_die('请登录后操作', 403); } // 验证reCAPTCHA(如果启用) if (get_option('wpiet_enable_recaptcha', false)) { $recaptcha_response = isset($_POST['g-recaptcha-response']) ? $_POST['g-recaptcha-response'] : ''; if (!self::verify_recaptcha($recaptcha_response)) { wp_send_json_error('验证码验证失败'); } } // 继续处理图片 self::save_image(); } private static function create_attachment($file_path, $filename) { $file_type = wp_check_filetype($filename, null); $attachment = [ 'post_mime_type' => $file_type['type'], 'post_title' => preg_replace('/.[^.]+$/', '', $filename), 'post_content' => '', 'post_status' => 'inherit', 'guid' => wp_get_upload_dir()['url'] . '/' . $filename ]; $attachment_id = wp_insert_attachment($attachment, $file_path); if (is_wp_error($attachment_id)) { return $attachment_id; } // 生成附件元数据 require_once(ABSPATH . 'wp-admin/includes/image.php'); $attachment_data = wp_generate_attachment_metadata($attachment_id, $file_path); wp_update_attachment_metadata($attachment_id, $attachment_data); return $attachment_id; } private static function verify_recaptcha($response) { $secret_key = get_option('wpiet_recaptcha_secret_key', ''); if (empty($secret_key)) { return true; } $verify_url = 'https://www.google.com/recaptcha/api/siteverify'; $verify_data = [ 'secret' => $secret_key, 'response' => $response, 'remoteip' => $_SERVER['REMOTE_ADDR'] ]; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $verify_url); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($verify_data)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); $result = curl_exec($ch); curl_close($ch); $result_data = json_decode($result, true); return isset($result_data['success']) && $result_data['success'] === true; } } 五、前端界面与用户体验优化 5.1 响应式编辑器界面 // public/js/editor-ui.js class EditorUI { constructor(editorInstance) { this.editor = editorInstance; this.uiState = { activeTool: null, isMobile: window.innerWidth < 768, isFullscreen: false, showSidebar: true }; this.initUI(); this.bindEvents(); this.adaptLayout(); } initUI() { // 创建工具栏 this.createToolbar(); // 创建侧边栏 this.createSidebar(); // 创建底部控制栏 this.createControls(); // 创建模态框 this.createModals(); } createToolbar() { const toolbarHTML = ` <div class="wpiet-toolbar"> <div class="toolbar-section file"> <button class="toolbar-btn" data-action="open"> <i class="icon-folder-open"></i> <span>打开</span> </button> <button class="toolbar-btn" data-action="save"> <i class="icon-save"></i> <span>保存</span> </button> <button class="toolbar-btn" data-action="export"> <i class="icon-download"></i> <span>导出</span> </button> </div> <div class="toolbar-section edit"> <button class="toolbar-btn" data-tool="crop"> <i class="icon-crop"></i> <span>裁剪</span> </button> <button class="toolbar-btn" data-tool="rotate"> <i class="icon-rotate-right"></i> <span>旋转</span> </button> <button class="toolbar-btn" data-tool="flip"> <i class="icon-flip-horizontal"></i> <span>翻转</span> </button> <button class="toolbar-btn" data-tool="adjust"> <i class="icon-sliders"></i> <span>调整</span> </button> </div> <div class="toolbar-section effects"> <button class="toolbar-btn" data-tool="filter"> <i class="icon-filter"></i> <span>滤镜</span> </button> <button class="toolbar-btn" data-tool="text"> <i class="icon-type"></i> <span>文字</span> </button> <button class="toolbar-btn" data-tool="sticker"> <i class="icon-sticker"></i> <span>贴图</span> </button> <button class="toolbar-btn" data-tool="frame"> <i class="icon-square"></i> <span>边框</span> </button> </div> <div class="toolbar-section view"> <button class="toolbar-btn" data-action="zoom-in"> <i class="icon-zoom-in"></i> </button> <button class="toolbar-btn" data-action="zoom-out"> <i class="icon-zoom-out"></i> </button> <button class="toolbar-btn" data-action="fullscreen"> <i class="icon-maximize"></i> </button> <button class="toolbar-btn" data-action="toggle-sidebar"> <i class="icon-sidebar"></i> </button> </div> </div> `; document.querySelector('.wpiet-editor-container').insertAdjacentHTML('afterbegin', toolbarHTML); } createSidebar() { const sidebarHTML = ` <div class="wpiet-sidebar ${this.uiState.showSidebar ? 'active' : ''}"> <div class="sidebar-header"> <h3>工具选项</h3> <button class="sidebar-close">&times;</button> </div> <div class="sidebar-content"> <!-- 动态内容 --> <div class="tool-options" id="tool-options"> <div class="empty-state"> <i class="icon-tool"></i> <p>选择一个工具开始编辑</p> </div> </div> <!-- 历史记录 --> <div class="history-section"> <h4>历史记录</h4> <div class="history-list" id="history-list"></div> <div class="history-controls"> <button class="btn-small" id="undo-btn" disabled> <i class="icon-undo"></i> 撤销 </button> <button class="btn-small" id="redo-btn" disabled> <i class="icon-redo"></i> 重做 </button> </div> </div> </div> </div> `; document.querySelector('.wpiet-editor-container').insertAdjacentHTML('beforeend', sidebarHTML); } bindEvents() { // 工具栏按钮点击 document.querySelectorAll('.toolbar-btn').forEach(btn => { btn.addEventListener('click', (e) => { const action = e.currentTarget.dataset.action; const tool = e.currentTarget.dataset.tool; if (action) { this.handleAction(action); } else if (tool) { this.activateTool(tool); } }); }); // 窗口大小变化 window.addEventListener('resize', () => { this.adaptLayout(); }); // 键盘快捷键 document.addEventListener('keydown', (e) => { this.handleKeyboardShortcuts(e); }); // 触摸设备支持 if ('ontouchstart' in window) { this.enableTouchSupport(); } } handleAction(action) { switch(action) { case 'open': this.openImagePicker(); break; case 'save': this.saveImage(); break; case 'export': this.exportImage(); break; case 'zoom-in': this.editor.zoomIn(); break; case 'zoom-out': this.editor.zoomOut(); break; case 'fullscreen': this.toggleFullscreen(); break; case 'toggle-sidebar': this.toggleSidebar(); break; } } activateTool(toolName) { // 更新UI状态 this.uiState.activeTool = toolName; // 更新工具栏按钮状态 document.querySelectorAll('.toolbar-btn').forEach(btn => {

发表评论

WordPress集成教程,连接开放API实现实时公共交通信息查询

WordPress集成教程:连接开放API实现实时公共交通信息查询 引言:为什么要在WordPress中集成公共交通查询功能 在当今数字化时代,网站已经不仅仅是信息展示的平台,更是提供实用工具和服务的重要渠道。对于城市生活类网站、旅游博客、本地商业网站或社区门户而言,集成实时公共交通信息查询功能可以显著提升用户体验和网站价值。 WordPress作为全球最流行的内容管理系统,其强大的扩展性使得开发者能够通过代码二次开发实现各种互联网小工具功能。本教程将详细介绍如何在WordPress中连接开放API,实现实时公共交通信息查询功能,为访问者提供便捷的出行规划服务。 通过本教程,您将学习到如何: 选择合适的公共交通API 在WordPress中安全地集成API 设计用户友好的查询界面 处理并展示实时交通数据 优化功能性能和用户体验 第一部分:准备工作与环境配置 1.1 选择合适的公共交通API 在开始开发之前,首先需要选择一个合适的公共交通API。以下是一些国内外常用的选择: 国际通用API: Google Maps Directions API:提供全球范围内的公共交通路线规划 Transitland:开源公共交通数据平台,覆盖多个国家和地区 OpenRouteService:基于开放数据提供路线规划服务 中国地区API: 高德地图API:提供全面的公共交通查询功能 百度地图API:包含公交、地铁等公共交通路线规划 腾讯地图API:公共交通查询功能完善 本教程将以高德地图API为例,因为其对中国公共交通数据的覆盖较为全面,且提供免费的开发额度。 1.2 注册API密钥 访问高德开放平台官网(https://lbs.amap.com/) 注册开发者账号并登录 进入控制台,创建新应用 获取您的API密钥(Key) 1.3 WordPress开发环境准备 确保您的WordPress环境满足以下条件: WordPress 5.0或更高版本 PHP 7.2或更高版本 已安装并激活一个适合开发的主题 具备基本的WordPress插件开发知识 1.4 创建插件目录结构 在WordPress的wp-content/plugins/目录下创建新插件文件夹public-transit-query,并建立以下结构: public-transit-query/ ├── public-transit-query.php ├── includes/ │ ├── class-api-handler.php │ ├── class-shortcode.php │ └── class-admin-settings.php ├── assets/ │ ├── css/ │ │ └── style.css │ └── js/ │ └── script.js └── templates/ └── transit-form.php 第二部分:创建WordPress插件框架 2.1 主插件文件配置 打开public-transit-query.php,添加以下基础插件代码: <?php /** * Plugin Name: 实时公共交通查询 * Plugin URI: https://yourwebsite.com/public-transit-query * Description: 在WordPress网站中集成实时公共交通查询功能 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later * Text Domain: public-transit-query */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('PTQ_VERSION', '1.0.0'); define('PTQ_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('PTQ_PLUGIN_URL', plugin_dir_url(__FILE__)); // 自动加载类文件 spl_autoload_register(function ($class_name) { if (strpos($class_name, 'PTQ_') === 0) { $class_file = PTQ_PLUGIN_DIR . 'includes/' . 'class-' . strtolower(str_replace('_', '-', $class_name)) . '.php'; if (file_exists($class_file)) { require_once $class_file; } } }); // 初始化插件 function ptq_init() { // 检查依赖 if (!function_exists('curl_init')) { add_action('admin_notices', function() { echo '<div class="notice notice-error"><p>公共交通查询插件需要cURL扩展支持,请确保您的PHP已启用cURL。</p></div>'; }); return; } // 初始化各个组件 PTQ_API_Handler::get_instance(); PTQ_Shortcode::get_instance(); PTQ_Admin_Settings::get_instance(); } add_action('plugins_loaded', 'ptq_init'); // 激活插件时的操作 register_activation_hook(__FILE__, 'ptq_activate'); function ptq_activate() { // 创建必要的数据库表或选项 if (!get_option('ptq_settings')) { $default_settings = array( 'api_key' => '', 'default_city' => '北京', 'cache_duration' => 300, 'enable_cache' => true ); update_option('ptq_settings', $default_settings); } } // 停用插件时的操作 register_deactivation_hook(__FILE__, 'ptq_deactivate'); function ptq_deactivate() { // 清理临时数据 delete_transient('ptq_api_status'); } 2.2 创建API处理类 在includes/class-api-handler.php中创建API处理类: <?php class PTQ_API_Handler { private static $instance = null; private $api_key; private $api_base = 'https://restapi.amap.com/v3/'; private $settings; public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } private function __construct() { $this->settings = get_option('ptq_settings', array()); $this->api_key = isset($this->settings['api_key']) ? $this->settings['api_key'] : ''; add_action('wp_ajax_ptq_get_transit', array($this, 'ajax_get_transit')); add_action('wp_ajax_nopriv_ptq_get_transit', array($this, 'ajax_get_transit')); } /** * 获取公共交通路线 */ public function get_transit_route($origin, $destination, $city = null) { if (empty($this->api_key)) { return new WP_Error('no_api_key', 'API密钥未配置'); } // 使用缓存减少API调用 $cache_key = 'ptq_route_' . md5($origin . $destination . $city); $cached_result = get_transient($cache_key); if ($cached_result !== false && isset($this->settings['enable_cache']) && $this->settings['enable_cache']) { return $cached_result; } // 构建API请求参数 $city = $city ?: (isset($this->settings['default_city']) ? $this->settings['default_city'] : '北京'); $params = array( 'key' => $this->api_key, 'origin' => $origin, 'destination' => $destination, 'city' => $city, 'extensions' => 'all', 'output' => 'JSON' ); $url = $this->api_base . 'direction/transit/integrated?' . http_build_query($params); // 发送API请求 $response = $this->make_request($url); if (is_wp_error($response)) { return $response; } // 解析响应数据 $result = $this->parse_transit_response($response); // 缓存结果 $cache_duration = isset($this->settings['cache_duration']) ? $this->settings['cache_duration'] : 300; set_transient($cache_key, $result, $cache_duration); return $result; } /** * 发送HTTP请求 */ private function make_request($url) { $args = array( 'timeout' => 15, 'headers' => array( 'Accept' => 'application/json' ) ); $response = wp_remote_get($url, $args); if (is_wp_error($response)) { return $response; } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if (json_last_error() !== JSON_ERROR_NONE) { return new WP_Error('json_parse_error', '解析API响应失败'); } if ($data['status'] != '1') { return new WP_Error('api_error', $data['info'] ?? 'API请求失败'); } return $data; } /** * 解析公共交通响应数据 */ private function parse_transit_response($data) { if (!isset($data['route']) || !isset($data['route']['transits'])) { return array(); } $transits = $data['route']['transits']; $parsed_routes = array(); foreach ($transits as $index => $transit) { if ($index >= 5) break; // 只显示前5条路线 $route = array( 'cost' => $transit['cost'] ?? '未知', 'duration' => $this->format_duration($transit['duration'] ?? 0), 'walking_distance' => $transit['walking_distance'] ?? 0, 'distance' => $transit['distance'] ?? 0, 'segments' => array() ); // 解析路线段 if (isset($transit['segments'])) { foreach ($transit['segments'] as $segment) { $segment_info = array( 'walking' => isset($segment['walking']) ? $segment['walking'] : null, 'bus' => isset($segment['bus']) ? $segment['bus'] : null, 'railway' => isset($segment['railway']) ? $segment['railway'] : null, 'taxi' => isset($segment['taxi']) ? $segment['taxi'] : null ); $route['segments'][] = $segment_info; } } $parsed_routes[] = $route; } return array( 'origin' => $data['route']['origin'] ?? '', 'destination' => $data['route']['destination'] ?? '', 'routes' => $parsed_routes, 'count' => count($parsed_routes) ); } /** * 格式化持续时间 */ private function format_duration($seconds) { $hours = floor($seconds / 3600); $minutes = floor(($seconds % 3600) / 60); if ($hours > 0) { return sprintf('%d小时%d分钟', $hours, $minutes); } else { return sprintf('%d分钟', $minutes); } } /** * AJAX处理函数 */ public function ajax_get_transit() { // 验证nonce if (!check_ajax_referer('ptq_ajax_nonce', 'nonce', false)) { wp_die('安全验证失败', 403); } $origin = sanitize_text_field($_POST['origin'] ?? ''); $destination = sanitize_text_field($_POST['destination'] ?? ''); $city = sanitize_text_field($_POST['city'] ?? ''); if (empty($origin) || empty($destination)) { wp_send_json_error('请输入起点和终点'); } $result = $this->get_transit_route($origin, $destination, $city); if (is_wp_error($result)) { wp_send_json_error($result->get_error_message()); } wp_send_json_success($result); } } 第三部分:创建短代码和前端界面 3.1 创建短代码类 在includes/class-shortcode.php中创建短代码处理类: <?php class PTQ_Shortcode { private static $instance = null; public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } private function __construct() { add_shortcode('public_transit_query', array($this, 'render_shortcode')); add_action('wp_enqueue_scripts', array($this, 'enqueue_assets')); } /** * 渲染短代码 */ public function render_shortcode($atts) { $atts = shortcode_atts(array( 'title' => '公共交通查询', 'default_city' => '', 'show_history' => 'true' ), $atts, 'public_transit_query'); ob_start(); include PTQ_PLUGIN_DIR . 'templates/transit-form.php'; return ob_get_clean(); } /** * 加载前端资源 */ public function enqueue_assets() { global $post; // 只在包含短代码的页面加载资源 if (is_a($post, 'WP_Post') && has_shortcode($post->post_content, 'public_transit_query')) { wp_enqueue_style( 'ptq-frontend-style', PTQ_PLUGIN_URL . 'assets/css/style.css', array(), PTQ_VERSION ); wp_enqueue_script( 'ptq-frontend-script', PTQ_PLUGIN_URL . 'assets/js/script.js', array('jquery'), PTQ_VERSION, true ); wp_localize_script('ptq-frontend-script', 'ptq_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('ptq_ajax_nonce'), 'loading_text' => '查询中...', 'error_text' => '查询失败,请重试', 'no_results_text' => '未找到相关路线' )); } } } 3.2 创建查询表单模板 在templates/transit-form.php中创建查询表单: <div class="ptq-container"> <div class="ptq-header"> <h2><?php echo esc_html($atts['title']); ?></h2> <p class="ptq-description">查询实时公共交通路线,规划您的出行</p> </div> <div class="ptq-form-container"> <form id="ptq-query-form" class="ptq-form"> <div class="ptq-form-row"> <div class="ptq-form-group"> <label for="ptq-origin">起点</label> <input type="text" id="ptq-origin" name="origin" placeholder="例如:天安门广场" required> <div class="ptq-example">可输入地址、地标或公交站名</div> </div> <div class="ptq-form-group"> <label for="ptq-destination">终点</label> <input type="text" id="ptq-destination" name="destination" placeholder="例如:北京西站" required> <div class="ptq-example">可输入地址、地标或公交站名</div> </div> </div> <div class="ptq-form-row"> <div class="ptq-form-group"> <label for="ptq-city">城市</label> <input type="text" id="ptq-city" name="city" placeholder="<?php echo esc_attr($atts['default_city'] ?: '北京'); ?>" value="<?php echo esc_attr($atts['default_city'] ?: ''); ?>"> <div class="ptq-example">默认为<?php echo esc_html($atts['default_city'] ?: '北京'); ?></div> </div> <div class="ptq-form-group ptq-submit-group"> <button type="submit" id="ptq-submit" class="ptq-submit-btn"> <span class="ptq-btn-text">查询路线</span> <span class="ptq-loading-spinner" style="display:none;"></span> </button> </div> </div> </form> </div> <div id="ptq-results-container" class="ptq-results-container" style="display:none;"> <div class="ptq-results-header"> <h3>查询结果</h3> <div class="ptq-route-summary"> <span id="ptq-route-origin"></span> → <span id="ptq-route-destination"></span> </div> </div> <div id="ptq-results" class="ptq-results"> <!-- 结果将通过JavaScript动态加载 --> </div> <div id="ptq-no-results" class="ptq-no-results" style="display:none;"> <p>未找到相关公共交通路线,请尝试调整查询条件。</p> </div> <div id="ptq-error" class="ptq-error" style="display:none;"> <p>查询过程中出现错误,请稍后重试。</p> </div> </div> <?php if ($atts['show_history'] === 'true') : ?> <div class="ptq-history-container"> <h4>最近查询</h4> <div id="ptq-history-list" class="ptq-history-list"> <!-- 查询历史将通过JavaScript动态加载 --> </div> </div> <?php endif; ?> <div class="ptq-footer"> <p class="ptq-disclaimer">数据来源:高德地图开放平台 | 更新时间:<span id="ptq-update-time"></span></p> </div> 第四部分:前端交互与样式设计 4.1 创建JavaScript交互脚本 在assets/js/script.js中添加前端交互逻辑: (function($) { 'use strict'; // 公共交通查询对象 var PTQ = { init: function() { this.cacheElements(); this.bindEvents(); this.loadHistory(); }, cacheElements: function() { this.$form = $('#ptq-query-form'); this.$origin = $('#ptq-origin'); this.$destination = $('#ptq-destination'); this.$city = $('#ptq-city'); this.$submitBtn = $('#ptq-submit'); this.$submitText = $('.ptq-btn-text'); this.$loadingSpinner = $('.ptq-loading-spinner'); this.$resultsContainer = $('#ptq-results-container'); this.$results = $('#ptq-results'); this.$noResults = $('#ptq-no-results'); this.$error = $('#ptq-error'); this.$historyList = $('#ptq-history-list'); this.$updateTime = $('#ptq-update-time'); }, bindEvents: function() { var self = this; // 表单提交事件 this.$form.on('submit', function(e) { e.preventDefault(); self.submitQuery(); }); // 输入框自动完成建议 this.setupAutocomplete(); // 更新当前时间 this.updateCurrentTime(); setInterval(function() { self.updateCurrentTime(); }, 60000); // 每分钟更新一次 }, submitQuery: function() { var self = this; var origin = this.$origin.val().trim(); var destination = this.$destination.val().trim(); var city = this.$city.val().trim(); // 验证输入 if (!origin || !destination) { this.showError('请输入起点和终点'); return; } // 显示加载状态 this.setLoading(true); // 隐藏之前的错误和结果 this.$noResults.hide(); this.$error.hide(); // 发送AJAX请求 $.ajax({ url: ptq_ajax.ajax_url, type: 'POST', dataType: 'json', data: { action: 'ptq_get_transit', nonce: ptq_ajax.nonce, origin: origin, destination: destination, city: city }, success: function(response) { self.setLoading(false); if (response.success) { self.displayResults(response.data); self.saveToHistory(origin, destination, city); } else { self.showError(response.data || ptq_ajax.error_text); } }, error: function() { self.setLoading(false); self.showError(ptq_ajax.error_text); } }); }, displayResults: function(data) { // 显示结果容器 this.$resultsContainer.show(); // 更新路线摘要 $('#ptq-route-origin').text(data.origin); $('#ptq-route-destination').text(data.destination); // 清空之前的结果 this.$results.empty(); if (!data.routes || data.routes.length === 0) { this.$noResults.show(); return; } // 显示路线结果 $.each(data.routes, function(index, route) { var routeHtml = self.buildRouteHtml(route, index + 1); self.$results.append(routeHtml); }); // 滚动到结果区域 $('html, body').animate({ scrollTop: self.$resultsContainer.offset().top - 100 }, 500); }, buildRouteHtml: function(route, index) { var html = '<div class="ptq-route-card">'; html += '<div class="ptq-route-header">'; html += '<span class="ptq-route-index">方案' + index + '</span>'; html += '<span class="ptq-route-stats">'; html += '<span class="ptq-stat"><i class="ptq-icon-time"></i>' + route.duration + '</span>'; html += '<span class="ptq-stat"><i class="ptq-icon-cost"></i>' + route.cost + '元</span>'; html += '<span class="ptq-stat"><i class="ptq-icon-distance"></i>' + (route.distance / 1000).toFixed(1) + '公里</span>'; html += '</span>'; html += '</div>'; html += '<div class="ptq-route-details">'; // 构建路线详情 if (route.segments && route.segments.length > 0) { $.each(route.segments, function(segmentIndex, segment) { html += self.buildSegmentHtml(segment, segmentIndex); }); } html += '</div>'; html += '</div>'; return html; }, buildSegmentHtml: function(segment, index) { var html = '<div class="ptq-segment">'; // 步行段 if (segment.walking && segment.walking.distance > 0) { html += '<div class="ptq-segment-walking">'; html += '<span class="ptq-segment-icon"><i class="ptq-icon-walk"></i></span>'; html += '<span class="ptq-segment-text">步行' + (segment.walking.distance / 1000).toFixed(1) + '公里</span>'; html += '</div>'; } // 公交段 if (segment.bus && segment.bus.buslines && segment.bus.buslines.length > 0) { $.each(segment.bus.buslines, function(i, busline) { html += '<div class="ptq-segment-bus">'; html += '<span class="ptq-segment-icon"><i class="ptq-icon-bus"></i></span>'; html += '<span class="ptq-segment-text">'; html += busline.name + ' (' + busline.departure_stop.name + ' → ' + busline.arrival_stop.name + ')'; html += '</span>'; html += '</div>'; }); } // 地铁段 if (segment.railway && segment.railway.name) { html += '<div class="ptq-segment-railway">'; html += '<span class="ptq-segment-icon"><i class="ptq-icon-subway"></i></span>'; html += '<span class="ptq-segment-text">' + segment.railway.name + '</span>'; html += '</div>'; } html += '</div>'; return html; }, setupAutocomplete: function() { // 这里可以集成高德地图的输入提示API // 由于篇幅限制,这里只提供基本思路 var self = this; // 使用高德地图的输入提示功能 // 需要额外引入高德地图JavaScript API if (typeof AMap !== 'undefined') { // 创建输入提示实例 var originAuto = new AMap.Autocomplete({ input: 'ptq-origin' }); var destAuto = new AMap.Autocomplete({ input: 'ptq-destination' }); // 监听选择事件 AMap.event.addListener(originAuto, 'select', function(e) { self.$origin.val(e.poi.name); }); AMap.event.addListener(destAuto, 'select', function(e) { self.$destination.val(e.poi.name); }); } }, saveToHistory: function(origin, destination, city) { var history = this.getHistory(); var query = { origin: origin, destination: destination, city: city, timestamp: new Date().getTime() }; // 添加到历史记录开头 history.unshift(query); // 只保留最近10条记录 if (history.length > 10) { history = history.slice(0, 10); } // 保存到localStorage localStorage.setItem('ptq_query_history', JSON.stringify(history)); // 更新显示 this.loadHistory(); }, getHistory: function() { var history = localStorage.getItem('ptq_query_history'); return history ? JSON.parse(history) : []; }, loadHistory: function() { var history = this.getHistory(); var self = this; this.$historyList.empty(); if (history.length === 0) { this.$historyList.append('<p class="ptq-no-history">暂无查询历史</p>'); return; } $.each(history, function(index, query) { var time = new Date(query.timestamp); var timeStr = time.getHours() + ':' + (time.getMinutes() < 10 ? '0' : '') + time.getMinutes(); var historyHtml = '<div class="ptq-history-item">'; historyHtml += '<div class="ptq-history-route">'; historyHtml += '<span class="ptq-history-origin">' + query.origin + '</span>'; historyHtml += ' → '; historyHtml += '<span class="ptq-history-destination">' + query.destination + '</span>'; historyHtml += '</div>'; historyHtml += '<div class="ptq-history-meta">'; historyHtml += '<span class="ptq-history-time">' + timeStr + '</span>'; historyHtml += '<button class="ptq-history-redo" data-origin="' + query.origin + '" data-destination="' + query.destination + '" data-city="' + (query.city || '') + '">再次查询</button>'; historyHtml += '</div>'; historyHtml += '</div>'; self.$historyList.append(historyHtml); }); // 绑定重新查询事件 $('.ptq-history-redo').on('click', function() { var $btn = $(this); self.$origin.val($btn.data('origin')); self.$destination.val($btn.data('destination')); self.$city.val($btn.data('city')); self.$form.submit(); }); }, setLoading: function(isLoading) { if (isLoading) { this.$submitText.text(ptq_ajax.loading_text); this.$loadingSpinner.show(); this.$submitBtn.prop('disabled', true); } else { this.$submitText.text('查询路线'); this.$loadingSpinner.hide(); this.$submitBtn.prop('disabled', false); } }, showError: function(message) { this.$error.find('p').text(message); this.$error.show(); }, updateCurrentTime: function() { var now = new Date(); var timeStr = now.getHours() + ':' + (now.getMinutes() < 10 ? '0' : '') + now.getMinutes(); this.$updateTime.text(timeStr); } }; // 初始化 $(document).ready(function() { PTQ.init(); }); // 暴露到全局 window.PTQ = PTQ; })(jQuery); 4.2 创建CSS样式文件 在assets/css/style.css中添加样式: /* 公共交通查询插件样式 */ .ptq-container { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background: #f8f9fa; border-radius: 12px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); } .ptq-header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 2px solid #e9ecef; } .ptq-header h2 { color: #2c3e50; margin: 0 0 10px 0; font-size: 28px; font-weight: 600; } .ptq-description { color: #6c757d; font-size: 16px; margin: 0; } /* 表单样式 */ .ptq-form-container { background: white; padding: 25px; border-radius: 10px; margin-bottom: 25px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); } .ptq-form-row { display: flex; flex-wrap: wrap; gap: 20px; margin-bottom: 20px; } .ptq-form-group { flex: 1; min-width: 250px; } .ptq-form-group label { display: block; margin-bottom: 8px; color: #495057; font-weight: 500; font-size: 14px; } .ptq-form-group input { width: 100%; padding: 12px 15px; border: 2px solid #dee2e6; border-radius: 6px; font-size: 16px; transition: all 0.3s ease; box-sizing: border-box; } .ptq-form-group input:focus { outline: none; border-color: #3498db; box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1); } .ptq-example { font-size: 12px; color: #6c757d; margin-top: 5px; font-style: italic; } .ptq-submit-group { display: flex; align-items: flex-end; } .ptq-submit-btn { background: linear-gradient(135deg, #3498db, #2980b9); color: white; border: none; padding: 14px 30px; border-radius: 6px; font-size: 16px; font-weight: 600; cursor: pointer; transition: all 0.3s ease; display: flex; align-items: center; justify-content: center; min-width: 150px; height: 48px; } .ptq-submit-btn:hover:not(:disabled) { background: linear-gradient(135deg, #2980b9, #1c6ea4); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3); } .ptq-submit-btn:disabled { opacity: 0.7; cursor: not-allowed; } .ptq-loading-spinner { width: 20px; height: 20px; border: 3px solid rgba(255, 255, 255, 0.3); border-radius: 50%; border-top-color: white; animation: ptq-spin 1s ease-in-out infinite; margin-left: 10px; } @keyframes ptq-spin { to { transform: rotate(360deg); } } /* 结果区域样式 */ .ptq-results-container { background: white; padding: 25px; border-radius: 10px; margin-bottom: 25px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); } .ptq-results-header { margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #e9ecef; } .ptq-results-header h3 { color: #2c3e50; margin: 0 0 10px 0; font-size: 22px; } .ptq-route-summary { color: #6c757d; font-size: 16px; font-weight: 500; } /* 路线卡片样式 */ .ptq-route-card { border: 1px solid #e9ecef; border-radius: 8px; padding: 20px; margin-bottom: 20px; transition: all 0.3s ease; } .ptq-route-card:hover { border-color: #3498db; box-shadow: 0 4px 15px rgba(52, 152, 219, 0.1); } .ptq-route-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 15px; border-bottom: 1px solid #f1f3f4; } .ptq-route-index { font-size: 18px; font-weight: 600; color: #2c3e50; } .ptq-route-stats { display: flex; gap: 20px; } .ptq-stat { display: flex; align-items: center; color: #6c757d; font-size: 14px; } .ptq-icon-time, .ptq-icon-cost, .ptq-icon-distance, .ptq-icon-walk, .ptq-icon-bus, .ptq-icon-subway { display: inline-block; width: 16px; height: 16px; margin-right: 6px; background-size: contain; background-repeat: no-repeat; }

发表评论

详细教程,为WordPress网站开发活动倒计时与预约提醒功能

WordPress网站开发活动倒计时与预约提醒功能详细教程 引言:为什么WordPress网站需要活动倒计时与预约提醒功能 在当今数字营销时代,网站互动功能已成为提升用户参与度和转化率的关键因素。活动倒计时与预约提醒功能作为常见的互联网小工具,能够有效创造紧迫感,提高用户参与活动的积极性。对于电商网站,倒计时可以促进限时抢购;对于活动策划网站,预约提醒能确保参与者不会错过重要事件。 WordPress作为全球最流行的内容管理系统,其强大的可扩展性使得开发者可以通过代码二次开发实现各种定制功能。本教程将详细指导您如何为WordPress网站开发活动倒计时与预约提醒功能,无需依赖昂贵的插件,完全通过自主代码实现。 第一部分:开发环境准备与基础架构设计 1.1 开发环境配置 在开始开发之前,请确保您的WordPress环境满足以下条件: WordPress版本5.0或更高 PHP版本7.4或更高(推荐8.0+) MySQL 5.6或更高版本 已安装并激活一个支持子主题的WordPress主题 1.2 创建功能插件 为了避免主题更新导致功能丢失,我们建议创建一个独立的功能插件: 在wp-content/plugins/目录下创建新文件夹event-countdown-manager 在该文件夹中创建主插件文件event-countdown-manager.php 添加插件头部信息: <?php /** * Plugin Name: 活动倒计时与预约提醒管理器 * Plugin URI: https://yourwebsite.com/ * Description: 为WordPress网站添加活动倒计时与预约提醒功能 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later * Text Domain: event-countdown-manager */ 1.3 数据库表设计 我们需要创建两个数据库表来存储活动信息和用户预约数据。在插件激活时创建这些表: // 创建数据库表 function ecm_create_database_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $events_table = $wpdb->prefix . 'ecm_events'; $bookings_table = $wpdb->prefix . 'ecm_bookings'; // 活动表SQL $events_sql = "CREATE TABLE IF NOT EXISTS $events_table ( event_id INT(11) NOT NULL AUTO_INCREMENT, event_title VARCHAR(255) NOT NULL, event_description TEXT, event_start DATETIME NOT NULL, event_end DATETIME NOT NULL, countdown_enabled TINYINT(1) DEFAULT 1, reminder_enabled TINYINT(1) DEFAULT 1, max_participants INT(11) DEFAULT 0, current_participants INT(11) DEFAULT 0, status ENUM('active', 'inactive', 'completed') DEFAULT 'active', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (event_id) ) $charset_collate;"; // 预约表SQL $bookings_sql = "CREATE TABLE IF NOT EXISTS $bookings_table ( booking_id INT(11) NOT NULL AUTO_INCREMENT, event_id INT(11) NOT NULL, user_id INT(11), user_name VARCHAR(255), user_email VARCHAR(255) NOT NULL, user_phone VARCHAR(50), reminder_sent TINYINT(1) DEFAULT 0, booking_time DATETIME DEFAULT CURRENT_TIMESTAMP, status ENUM('confirmed', 'pending', 'cancelled') DEFAULT 'confirmed', PRIMARY KEY (booking_id), FOREIGN KEY (event_id) REFERENCES $events_table(event_id) ON DELETE CASCADE ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($events_sql); dbDelta($bookings_sql); } register_activation_hook(__FILE__, 'ecm_create_database_tables'); 第二部分:后台管理界面开发 2.1 创建活动管理菜单 为管理员添加活动管理界面: // 添加管理菜单 function ecm_add_admin_menu() { add_menu_page( '活动倒计时管理', '活动管理', 'manage_options', 'ecm-events', 'ecm_events_admin_page', 'dashicons-calendar-alt', 30 ); add_submenu_page( 'ecm-events', '添加新活动', '添加新活动', 'manage_options', 'ecm-add-event', 'ecm_add_event_page' ); add_submenu_page( 'ecm-events', '预约管理', '预约管理', 'manage_options', 'ecm-bookings', 'ecm_bookings_admin_page' ); } add_action('admin_menu', 'ecm_add_admin_menu'); 2.2 活动列表管理页面 创建活动列表显示与管理页面: function ecm_events_admin_page() { global $wpdb; $events_table = $wpdb->prefix . 'ecm_events'; // 处理删除操作 if (isset($_GET['action']) && $_GET['action'] == 'delete' && isset($_GET['event_id'])) { $event_id = intval($_GET['event_id']); $wpdb->delete($events_table, array('event_id' => $event_id)); echo '<div class="notice notice-success"><p>活动已删除</p></div>'; } // 获取所有活动 $events = $wpdb->get_results("SELECT * FROM $events_table ORDER BY event_start DESC"); ?> <div class="wrap"> <h1 class="wp-heading-inline">活动管理</h1> <a href="<?php echo admin_url('admin.php?page=ecm-add-event'); ?>" class="page-title-action">添加新活动</a> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <th>ID</th> <th>活动标题</th> <th>开始时间</th> <th>结束时间</th> <th>当前/最大参与人数</th> <th>状态</th> <th>操作</th> </tr> </thead> <tbody> <?php if (empty($events)): ?> <tr> <td colspan="7" style="text-align: center;">暂无活动</td> </tr> <?php else: ?> <?php foreach ($events as $event): ?> <tr> <td><?php echo $event->event_id; ?></td> <td><?php echo esc_html($event->event_title); ?></td> <td><?php echo date('Y-m-d H:i', strtotime($event->event_start)); ?></td> <td><?php echo date('Y-m-d H:i', strtotime($event->event_end)); ?></td> <td><?php echo $event->current_participants . '/' . $event->max_participants; ?></td> <td> <?php $status_labels = array( 'active' => '<span class="dashicons dashicons-yes-alt" style="color:green"></span> 进行中', 'inactive' => '<span class="dashicons dashicons-no" style="color:red"></span> 未激活', 'completed' => '<span class="dashicons dashicons-yes" style="color:blue"></span> 已结束' ); echo $status_labels[$event->status]; ?> </td> <td> <a href="<?php echo admin_url('admin.php?page=ecm-add-event&event_id=' . $event->event_id); ?>">编辑</a> | <a href="<?php echo admin_url('admin.php?page=ecm-events&action=delete&event_id=' . $event->event_id); ?>" onclick="return confirm('确定删除此活动吗?')">删除</a> </td> </tr> <?php endforeach; ?> <?php endif; ?> </tbody> </table> </div> <?php } 2.3 添加/编辑活动页面 创建活动添加与编辑表单: function ecm_add_event_page() { global $wpdb; $events_table = $wpdb->prefix . 'ecm_events'; $event_id = isset($_GET['event_id']) ? intval($_GET['event_id']) : 0; $event = null; if ($event_id > 0) { $event = $wpdb->get_row($wpdb->prepare("SELECT * FROM $events_table WHERE event_id = %d", $event_id)); } // 处理表单提交 if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ecm_event_nonce'])) { if (!wp_verify_nonce($_POST['ecm_event_nonce'], 'ecm_save_event')) { die('安全验证失败'); } $event_data = array( 'event_title' => sanitize_text_field($_POST['event_title']), 'event_description' => wp_kses_post($_POST['event_description']), 'event_start' => sanitize_text_field($_POST['event_start']), 'event_end' => sanitize_text_field($_POST['event_end']), 'countdown_enabled' => isset($_POST['countdown_enabled']) ? 1 : 0, 'reminder_enabled' => isset($_POST['reminder_enabled']) ? 1 : 0, 'max_participants' => intval($_POST['max_participants']), 'status' => sanitize_text_field($_POST['status']) ); if ($event_id > 0) { $wpdb->update($events_table, $event_data, array('event_id' => $event_id)); $message = '活动已更新'; } else { $wpdb->insert($events_table, $event_data); $event_id = $wpdb->insert_id; $message = '活动已创建'; } echo '<div class="notice notice-success"><p>' . $message . '</p></div>'; $event = $wpdb->get_row($wpdb->prepare("SELECT * FROM $events_table WHERE event_id = %d", $event_id)); } ?> <div class="wrap"> <h1><?php echo $event_id > 0 ? '编辑活动' : '添加新活动'; ?></h1> <form method="post" action=""> <?php wp_nonce_field('ecm_save_event', 'ecm_event_nonce'); ?> <table class="form-table"> <tr> <th><label for="event_title">活动标题</label></th> <td> <input type="text" id="event_title" name="event_title" class="regular-text" value="<?php echo $event ? esc_attr($event->event_title) : ''; ?>" required> </td> </tr> <tr> <th><label for="event_description">活动描述</label></th> <td> <?php $description = $event ? $event->event_description : ''; wp_editor($description, 'event_description', array( 'textarea_name' => 'event_description', 'media_buttons' => false, 'textarea_rows' => 5 )); ?> </td> </tr> <tr> <th><label for="event_start">开始时间</label></th> <td> <input type="datetime-local" id="event_start" name="event_start" value="<?php echo $event ? date('Y-m-dTH:i', strtotime($event->event_start)) : ''; ?>" required> </td> </tr> <tr> <th><label for="event_end">结束时间</label></th> <td> <input type="datetime-local" id="event_end" name="event_end" value="<?php echo $event ? date('Y-m-dTH:i', strtotime($event->event_end)) : ''; ?>" required> </td> </tr> <tr> <th>功能设置</th> <td> <label> <input type="checkbox" name="countdown_enabled" value="1" <?php echo ($event && $event->countdown_enabled) ? 'checked' : ''; ?>> 启用倒计时 </label> <br> <label> <input type="checkbox" name="reminder_enabled" value="1" <?php echo ($event && $event->reminder_enabled) ? 'checked' : ''; ?>> 启用预约提醒 </label> </td> </tr> <tr> <th><label for="max_participants">最大参与人数</label></th> <td> <input type="number" id="max_participants" name="max_participants" min="0" value="<?php echo $event ? $event->max_participants : '0'; ?>"> <p class="description">0表示不限制人数</p> </td> </tr> <tr> <th><label for="status">状态</label></th> <td> <select id="status" name="status"> <option value="active" <?php echo ($event && $event->status == 'active') ? 'selected' : ''; ?>>进行中</option> <option value="inactive" <?php echo ($event && $event->status == 'inactive') ? 'selected' : ''; ?>>未激活</option> <option value="completed" <?php echo ($event && $event->status == 'completed') ? 'selected' : ''; ?>>已结束</option> </select> </td> </tr> </table> <p class="submit"> <input type="submit" class="button button-primary" value="保存活动"> <a href="<?php echo admin_url('admin.php?page=ecm-events'); ?>" class="button">返回列表</a> </p> </form> </div> <?php } 第三部分:前端倒计时功能实现 3.1 创建短代码显示倒计时 创建短代码以便在文章或页面中插入倒计时: // 注册倒计时短代码 function ecm_countdown_shortcode($atts) { global $wpdb; $atts = shortcode_atts(array( 'event_id' => 0, 'title' => '活动倒计时', 'show_description' => true, 'style' => 'default' ), $atts); $event_id = intval($atts['event_id']); $events_table = $wpdb->prefix . 'ecm_events'; if ($event_id === 0) { // 获取最近的活动 $event = $wpdb->get_row("SELECT * FROM $events_table WHERE status = 'active' AND event_end > NOW() ORDER BY event_start LIMIT 1"); } else { $event = $wpdb->get_row($wpdb->prepare("SELECT * FROM $events_table WHERE event_id = %d", $event_id)); } if (!$event || $event->countdown_enabled != 1) { return '<p>暂无活动或倒计时未启用</p>'; } // 生成唯一ID用于JavaScript $countdown_id = 'ecm-countdown-' . uniqid(); ob_start(); ?> <div class="ecm-countdown-container ecm-style-<?php echo esc_attr($atts['style']); ?>" id="<?php echo $countdown_id; ?>"> <div class="ecm-countdown-header"> <h3><?php echo esc_html($atts['title']); ?></h3> <h4><?php echo esc_html($event->event_title); ?></h4> </div> <?php if ($atts['show_description'] && !empty($event->event_description)): ?> <div class="ecm-countdown-description"> <?php echo wpautop($event->event_description); ?> </div> <?php endif; ?> <div class="ecm-countdown-timer"> <div class="ecm-countdown-item"> <span class="ecm-countdown-number ecm-days">00</span> <span class="ecm-countdown-label">天</span> </div> <div class="ecm-countdown-separator">:</div> <div class="ecm-countdown-item"> <span class="ecm-countdown-number ecm-hours">00</span> <span class="ecm-countdown-label">时</span> </div> <div class="ecm-countdown-separator">:</div> <div class="ecm-countdown-item"> <span class="ecm-countdown-number ecm-minutes">00</span> <span class="ecm-countdown-label">分</span> </div> <div class="ecm-countdown-separator">:</div> <div class="ecm-countdown-item"> <span class="ecm-countdown-number ecm-seconds">00</span> <span class="ecm-countdown-label">秒</span> </div> </div> <div class="ecm-countdown-message"></div> <?php if ($event->reminder_enabled): ?> <div class="ecm-reminder-section"> <button class="ecm-reminder-btn" data-event-id="<?php echo $event->event_id; ?>"> 设置活动提醒 </button> </div> <?php endif; ?> 3.2 倒计时JavaScript实现 继续在短代码函数中添加JavaScript代码: <script> (function() { var countdownElement = document.getElementById('<?php echo $countdown_id; ?>'); var eventEndTime = new Date('<?php echo $event->event_end; ?>').getTime(); function updateCountdown() { var now = new Date().getTime(); var timeRemaining = eventEndTime - now; if (timeRemaining < 0) { countdownElement.querySelector('.ecm-countdown-message').innerHTML = '<p class="ecm-countdown-ended">活动已结束</p>'; countdownElement.querySelector('.ecm-countdown-timer').style.display = 'none'; return; } var days = Math.floor(timeRemaining / (1000 * 60 * 60 * 24)); var hours = Math.floor((timeRemaining % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); var minutes = Math.floor((timeRemaining % (1000 * 60 * 60)) / (1000 * 60)); var seconds = Math.floor((timeRemaining % (1000 * 60)) / 1000); // 更新显示 countdownElement.querySelector('.ecm-days').textContent = days.toString().padStart(2, '0'); countdownElement.querySelector('.ecm-hours').textContent = hours.toString().padStart(2, '0'); countdownElement.querySelector('.ecm-minutes').textContent = minutes.toString().padStart(2, '0'); countdownElement.querySelector('.ecm-seconds').textContent = seconds.toString().padStart(2, '0'); // 动态样式变化 if (days === 0 && hours < 24) { countdownElement.classList.add('ecm-countdown-urgent'); } if (days === 0 && hours < 1) { countdownElement.classList.add('ecm-countdown-critical'); } } // 初始更新 updateCountdown(); // 每秒更新 var countdownInterval = setInterval(updateCountdown, 1000); // 预约提醒按钮事件 var reminderBtn = countdownElement.querySelector('.ecm-reminder-btn'); if (reminderBtn) { reminderBtn.addEventListener('click', function() { var eventId = this.getAttribute('data-event-id'); ecmShowReminderModal(eventId); }); } })(); </script> <div class="ecm-reminder-modal" style="display: none;"> <div class="ecm-modal-content"> <span class="ecm-close-modal">&times;</span> <h3>设置活动提醒</h3> <form id="ecm-reminder-form"> <input type="hidden" name="event_id" value="<?php echo $event->event_id; ?>"> <div class="ecm-form-group"> <label for="ecm-user-name">姓名</label> <input type="text" id="ecm-user-name" name="user_name" required> </div> <div class="ecm-form-group"> <label for="ecm-user-email">邮箱</label> <input type="email" id="ecm-user-email" name="user_email" required> </div> <div class="ecm-form-group"> <label for="ecm-user-phone">手机号(可选)</label> <input type="tel" id="ecm-user-phone" name="user_phone"> </div> <div class="ecm-form-group"> <label> <input type="checkbox" name="email_reminder" checked> 通过邮件提醒 </label> </div> <button type="submit" class="ecm-submit-btn">确认预约</button> </form> </div> </div> </div> <style> .ecm-countdown-container { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 15px; padding: 30px; color: white; text-align: center; margin: 20px 0; box-shadow: 0 10px 30px rgba(0,0,0,0.2); } .ecm-countdown-header h3 { margin: 0 0 10px 0; font-size: 24px; } .ecm-countdown-header h4 { margin: 0 0 20px 0; font-size: 20px; opacity: 0.9; } .ecm-countdown-timer { display: flex; justify-content: center; align-items: center; margin: 30px 0; font-family: 'Courier New', monospace; } .ecm-countdown-item { display: flex; flex-direction: column; align-items: center; margin: 0 10px; } .ecm-countdown-number { font-size: 48px; font-weight: bold; background: rgba(255,255,255,0.1); padding: 10px 20px; border-radius: 10px; min-width: 80px; display: inline-block; } .ecm-countdown-label { margin-top: 5px; font-size: 14px; opacity: 0.8; } .ecm-countdown-separator { font-size: 36px; margin: 0 5px; opacity: 0.7; } .ecm-countdown-urgent .ecm-countdown-number { background: rgba(255,193,7,0.2); color: #ffc107; } .ecm-countdown-critical .ecm-countdown-number { background: rgba(220,53,69,0.2); color: #dc3545; animation: pulse 1s infinite; } @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.7; } 100% { opacity: 1; } } .ecm-reminder-btn { background: white; color: #667eea; border: none; padding: 12px 30px; border-radius: 50px; font-size: 16px; font-weight: bold; cursor: pointer; transition: all 0.3s ease; } .ecm-reminder-btn:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0,0,0,0.3); } .ecm-reminder-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); display: flex; justify-content: center; align-items: center; z-index: 10000; } .ecm-modal-content { background: white; padding: 30px; border-radius: 10px; width: 90%; max-width: 500px; position: relative; color: #333; } .ecm-close-modal { position: absolute; top: 15px; right: 20px; font-size: 24px; cursor: pointer; } .ecm-form-group { margin-bottom: 20px; } .ecm-form-group label { display: block; margin-bottom: 5px; font-weight: bold; } .ecm-form-group input[type="text"], .ecm-form-group input[type="email"], .ecm-form-group input[type="tel"] { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 16px; } .ecm-submit-btn { background: #667eea; color: white; border: none; padding: 12px 30px; border-radius: 5px; font-size: 16px; cursor: pointer; width: 100%; } .ecm-submit-btn:hover { background: #5a67d8; } </style> <?php return ob_get_clean(); } add_shortcode('event_countdown', 'ecm_countdown_shortcode'); 3.3 添加全局JavaScript函数 在插件中注册全局JavaScript函数: // 添加前端脚本 function ecm_enqueue_frontend_scripts() { wp_enqueue_style('ecm-frontend-style', plugins_url('css/ecm-frontend.css', __FILE__)); wp_enqueue_script('ecm-frontend-script', plugins_url('js/ecm-frontend.js', __FILE__), array('jquery'), '1.0.0', true); wp_localize_script('ecm-frontend-script', 'ecm_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('ecm_ajax_nonce') )); } add_action('wp_enqueue_scripts', 'ecm_enqueue_frontend_scripts'); 创建JavaScript文件 js/ecm-frontend.js: // 显示预约提醒模态框 function ecmShowReminderModal(eventId) { var modal = document.createElement('div'); modal.className = 'ecm-reminder-modal'; modal.innerHTML = ` <div class="ecm-modal-content"> <span class="ecm-close-modal" onclick="this.parentElement.parentElement.remove()">&times;</span> <h3>设置活动提醒</h3> <form id="ecm-reminder-form"> <input type="hidden" name="event_id" value="${eventId}"> <div class="ecm-form-group"> <label for="ecm-user-name">姓名</label> <input type="text" id="ecm-user-name" name="user_name" required> </div> <div class="ecm-form-group"> <label for="ecm-user-email">邮箱</label> <input type="email" id="ecm-user-email" name="user_email" required> </div> <div class="ecm-form-group"> <label for="ecm-user-phone">手机号(可选)</label> <input type="tel" id="ecm-user-email" name="user_phone"> </div> <div class="ecm-form-group"> <label> <input type="checkbox" name="email_reminder" checked> 通过邮件提醒 </label> </div> <button type="submit" class="ecm-submit-btn">确认预约</button> </form> </div> `; document.body.appendChild(modal); // 表单提交处理 modal.querySelector('#ecm-reminder-form').addEventListener('submit', function(e) { e.preventDefault(); ecmSubmitReminder(this); }); // 点击模态框外部关闭 modal.addEventListener('click', function(e) { if (e.target === this) { this.remove(); } }); } // 提交预约提醒 function ecmSubmitReminder(form) { var formData = new FormData(form); var submitBtn = form.querySelector('.ecm-submit-btn'); var originalText = submitBtn.textContent; submitBtn.textContent = '提交中...'; submitBtn.disabled = true; // 添加AJAX请求 formData.append('action', 'ecm_submit_reminder'); formData.append('nonce', ecm_ajax.nonce); fetch(ecm_ajax.ajax_url, { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { if (data.success) { form.innerHTML = ` <div class="ecm-success-message"> <div style="text-align: center; padding: 20px;"> <div style="font-size: 48px; color: #28a745;">✓</div> <h3>预约成功!</h3> <p>${data.message}</p> <button onclick="this.closest('.ecm-reminder-modal').remove()" class="ecm-submit-btn" style="margin-top: 20px;"> 关闭 </button> </div> </div> `; } else { alert('错误:' + data.message); submitBtn.textContent = originalText; submitBtn.disabled = false; } }) .catch(error => { console.error('Error:', error); alert('提交失败,请稍后重试'); submitBtn.textContent = originalText; submitBtn.disabled = false; }); } 第四部分:预约提醒系统实现 4.1 处理预约表单提交 创建AJAX处理函数: // 处理预约表单提交 function ecm_handle_reminder_submission() { // 验证nonce if (!wp_verify_nonce($_POST['nonce'], 'ecm_ajax_nonce')) { wp_die('安全验证失败'); } global $wpdb; $events_table = $wpdb->prefix . 'ecm_events'; $bookings_table = $wpdb->prefix . 'ecm_bookings'; $event_id = intval($_POST['event_id']); $user_name = sanitize_text_field($_POST['user_name']); $user_email = sanitize_email($_POST['user_email']); $user_phone = sanitize_text_field($_POST['user_phone']); // 验证数据 if (empty($user_name) || empty($user_email)) { wp_send_json_error(array('message' => '请填写必填字段')); } if (!is_email($user_email)) { wp_send_json_error(array('message' => '邮箱格式不正确')); } // 检查活动是否存在且可预约 $event = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $events_table WHERE event_id = %d AND status = 'active' AND reminder_enabled = 1", $event_id )); if (!$event) { wp_send_json_error(array('message' => '活动不存在或已结束预约')); } // 检查是否已满员 if ($event->max_participants > 0 && $event->current_participants >= $event->max_participants) { wp_send_json_error(array('message' => '活动人数已满')); } // 检查是否已预约 $existing_booking = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $bookings_table WHERE event_id = %d AND user_email = %s AND status != 'cancelled'", $event_id, $user_email )); if ($existing_booking > 0) { wp_send_json_error(array('message' => '您已经预约过此活动')); } // 获取当前用户ID(如果已登录) $user_id = is_user_logged_in() ? get_current_user_id() : 0; // 插入预约记录 $booking_data = array( 'event_id' => $event_id, 'user_id' => $user_id, 'user_name' => $user_name, 'user_email' => $user_email, 'user_phone' => $user_phone, 'status' => 'confirmed' ); $result = $wpdb->insert($bookings_table, $booking_data); if ($result) { // 更新活动参与人数 $wpdb->query($wpdb->prepare( "UPDATE $events_table SET current_participants = current_participants + 1 WHERE event_id = %d", $event_id )); // 发送确认邮件 if (isset($_POST['email_reminder'])) { ecm_send_confirmation_email($user_email, $user_name, $event); } wp_send_json_success(array( 'message' => '预约成功!活动开始前您将收到提醒。' )); } else { wp_send_json_error(array('message' => '预约失败,请稍后重试')); } } add_action('wp_ajax_ecm_submit_reminder', 'ecm_handle_reminder_submission'); add_action('wp_ajax_nopriv_ecm_submit_reminder', 'ecm_handle_reminder_submission'); 4.2 发送确认邮件功能 // 发送确认邮件 function ecm_send_confirmation_email($user_email, $user_name, $event) { $subject = '活动预约确认:' . $event->event_title; $message = ' <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>活动预约确认</title> <style> body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; } .container { max-width: 600px; margin: 0 auto; padding: 20px; } .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; text-align: center; border-radius: 10px 10px 0 0; } .content { background: #f9f9f9; padding: 30px; border-radius: 0 0 10px 10px; } .event-info { background: white; padding: 20px; border-radius: 5px; margin: 20px 0; border-left: 4px solid #667eea; }

发表评论

一步步教你,集成在线字体预览与个性化字库管理工具到网站

一步步教你,集成在线字体预览与个性化字库管理工具到网站,通过WordPress程序的代码二次开发实现常用互联网小工具功能 引言:字体在网站设计中的重要性 在当今数字化时代,网站设计已成为品牌形象和用户体验的重要组成部分。字体作为视觉传达的核心元素之一,直接影响着网站的可读性、美观性和品牌识别度。然而,对于许多网站管理员和设计师来说,字体管理一直是一个挑战——如何在保持网站性能的同时,提供丰富的字体选择?如何让用户能够实时预览字体效果?如何高效管理自定义字库? 本文将详细介绍如何通过WordPress代码二次开发,集成在线字体预览与个性化字库管理工具到您的网站中。我们将从基础概念讲起,逐步深入到具体实现步骤,帮助您打造一个功能完善、用户友好的字体管理系统。 第一部分:准备工作与环境搭建 1.1 理解WordPress字体管理现状 WordPress默认提供有限的字体选择,通常依赖于主题预设或Google Fonts等外部服务。虽然这些方案简单易用,但存在以下局限性: 字体选择受限,难以满足个性化需求 缺乏实时预览功能,用户无法直观感受字体效果 外部字体服务可能影响页面加载速度 难以管理自定义品牌字体 1.2 开发环境准备 在开始开发之前,请确保您已准备好以下环境: 本地开发环境:推荐使用XAMPP、MAMP或Local by Flywheel WordPress安装:建议使用最新版本的WordPress 代码编辑器:如VS Code、Sublime Text或PHPStorm 浏览器开发者工具:用于调试和测试 FTP客户端:用于将文件上传到生产环境 1.3 创建开发子主题 为了避免直接修改主题文件导致更新时丢失更改,我们首先创建一个子主题: 在WordPress的wp-content/themes/目录下创建新文件夹,命名为my-font-manager-theme 在该文件夹中创建style.css文件,添加以下内容: /* Theme Name: My Font Manager Theme Template: twentytwentythree // 根据您使用的父主题修改 Version: 1.0.0 Description: 子主题用于集成字体管理功能 */ 创建functions.php文件,用于添加自定义功能 第二部分:构建字体预览功能 2.1 设计字体预览界面 字体预览功能需要直观展示不同字体的效果。我们将创建一个简单的预览界面: // 在functions.php中添加字体预览短代码 function font_preview_shortcode($atts) { $atts = shortcode_atts(array( 'text' => '预览文本', 'size' => '24px', 'color' => '#333333', 'font' => 'Arial, sans-serif' ), $atts); $output = '<div class="font-preview-container">'; $output .= '<div class="font-preview-controls">'; $output .= '<input type="text" class="font-preview-text" value="' . esc_attr($atts['text']) . '" placeholder="输入预览文本">'; $output .= '<input type="range" class="font-preview-size" min="12" max="72" value="' . str_replace('px', '', $atts['size']) . '">'; $output .= '<input type="color" class="font-preview-color" value="' . esc_attr($atts['color']) . '">'; $output .= '<select class="font-preview-select">'; $output .= '<option value="Arial, sans-serif"' . selected($atts['font'], 'Arial, sans-serif', false) . '>Arial</option>'; $output .= '<option value="Georgia, serif"' . selected($atts['font'], 'Georgia, serif', false) . '>Georgia</option>'; // 更多字体选项将在后续添加 $output .= '</select>'; $output .= '</div>'; $output .= '<div class="font-preview-display" style="font-family: ' . esc_attr($atts['font']) . '; font-size: ' . esc_attr($atts['size']) . '; color: ' . esc_attr($atts['color']) . ';">'; $output .= esc_html($atts['text']); $output .= '</div>'; $output .= '</div>'; return $output; } add_shortcode('font_preview', 'font_preview_shortcode'); 2.2 添加交互功能 通过JavaScript实现实时预览效果: // 创建js/font-preview.js文件 jQuery(document).ready(function($) { $('.font-preview-container').each(function() { var $container = $(this); var $display = $container.find('.font-preview-display'); var $textInput = $container.find('.font-preview-text'); var $sizeInput = $container.find('.font-preview-size'); var $colorInput = $container.find('.font-preview-color'); var $fontSelect = $container.find('.font-preview-select'); // 更新预览文本 $textInput.on('input', function() { $display.text($(this).val()); }); // 更新字体大小 $sizeInput.on('input', function() { $display.css('font-size', $(this).val() + 'px'); }); // 更新字体颜色 $colorInput.on('input', function() { $display.css('color', $(this).val()); }); // 更新字体族 $fontSelect.on('change', function() { $display.css('font-family', $(this).val()); }); }); }); 2.3 集成Google Fonts API 为了提供更多字体选择,我们可以集成Google Fonts API: // 在functions.php中添加Google Fonts集成 function enqueue_google_fonts() { wp_enqueue_style('google-fonts', 'https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&family=Open+Sans:wght@300;400;600&family=Montserrat:wght@400;700&display=swap'); } add_action('wp_enqueue_scripts', 'enqueue_google_fonts'); // 扩展字体选择选项 function get_available_fonts() { $fonts = array( '系统字体' => array( 'Arial, sans-serif' => 'Arial', 'Georgia, serif' => 'Georgia', 'Times New Roman, serif' => 'Times New Roman', 'Verdana, sans-serif' => 'Verdana' ), 'Google字体' => array( 'Roboto, sans-serif' => 'Roboto', 'Open Sans, sans-serif' => 'Open Sans', 'Montserrat, sans-serif' => 'Montserrat', 'Lato, sans-serif' => 'Lato', 'Poppins, sans-serif' => 'Poppins' ) ); return $fonts; } // 更新短代码以包含更多字体选项 function font_preview_shortcode_enhanced($atts) { $atts = shortcode_atts(array( 'text' => '预览文本', 'size' => '24px', 'color' => '#333333', 'font' => 'Arial, sans-serif' ), $atts); $fonts = get_available_fonts(); $output = '<div class="font-preview-container">'; $output .= '<div class="font-preview-controls">'; $output .= '<input type="text" class="font-preview-text" value="' . esc_attr($atts['text']) . '" placeholder="输入预览文本">'; $output .= '<input type="range" class="font-preview-size" min="12" max="72" value="' . str_replace('px', '', $atts['size']) . '">'; $output .= '<input type="color" class="font-preview-color" value="' . esc_attr($atts['color']) . '">'; $output .= '<select class="font-preview-select">'; foreach ($fonts as $category => $font_list) { $output .= '<optgroup label="' . esc_attr($category) . '">'; foreach ($font_list as $font_value => $font_name) { $selected = $font_value === $atts['font'] ? 'selected' : ''; $output .= '<option value="' . esc_attr($font_value) . '" ' . $selected . '>' . esc_html($font_name) . '</option>'; } $output .= '</optgroup>'; } $output .= '</select>'; $output .= '</div>'; $output .= '<div class="font-preview-display" style="font-family: ' . esc_attr($atts['font']) . '; font-size: ' . esc_attr($atts['size']) . '; color: ' . esc_attr($atts['color']) . ';">'; $output .= esc_html($atts['text']); $output .= '</div>'; $output .= '</div>'; return $output; } add_shortcode('font_preview_enhanced', 'font_preview_shortcode_enhanced'); 第三部分:构建字库管理系统 3.1 设计数据库结构 为了管理自定义字体,我们需要创建数据库表来存储字体信息: // 创建字体数据库表 function create_fonts_table() { global $wpdb; $table_name = $wpdb->prefix . 'custom_fonts'; $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, font_name varchar(100) NOT NULL, font_family varchar(100) NOT NULL, font_file_woff2 varchar(255), font_file_woff varchar(255), font_file_ttf varchar(255), font_license text, upload_date datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, is_active tinyint(1) DEFAULT 1 NOT NULL, PRIMARY KEY (id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } register_activation_hook(__FILE__, 'create_fonts_table'); 3.2 创建字体上传和管理界面 我们将创建一个WordPress管理页面来管理自定义字体: // 添加字体管理菜单 function add_font_management_menu() { add_menu_page( '字体管理', '字体管理', 'manage_options', 'font-management', 'font_management_page', 'dashicons-editor-textcolor', 30 ); add_submenu_page( 'font-management', '添加新字体', '添加新字体', 'manage_options', 'add-new-font', 'add_new_font_page' ); } add_action('admin_menu', 'add_font_management_menu'); // 字体管理主页面 function font_management_page() { global $wpdb; $table_name = $wpdb->prefix . 'custom_fonts'; $fonts = $wpdb->get_results("SELECT * FROM $table_name ORDER BY upload_date DESC"); echo '<div class="wrap">'; echo '<h1>自定义字体管理</h1>'; echo '<a href="' . admin_url('admin.php?page=add-new-font') . '" class="button button-primary">添加新字体</a>'; echo '<hr>'; if (empty($fonts)) { echo '<p>暂无自定义字体。请点击上方按钮添加字体。</p>'; } else { echo '<table class="wp-list-table widefat fixed striped">'; echo '<thead><tr>'; echo '<th>ID</th><th>字体名称</th><th>字体族</th><th>文件格式</th><th>上传日期</th><th>状态</th><th>操作</th>'; echo '</tr></thead>'; echo '<tbody>'; foreach ($fonts as $font) { $formats = array(); if ($font->font_file_woff2) $formats[] = 'WOFF2'; if ($font->font_file_woff) $formats[] = 'WOFF'; if ($font->font_file_ttf) $formats[] = 'TTF'; echo '<tr>'; echo '<td>' . $font->id . '</td>'; echo '<td>' . esc_html($font->font_name) . '</td>'; echo '<td>' . esc_html($font->font_family) . '</td>'; echo '<td>' . implode(', ', $formats) . '</td>'; echo '<td>' . $font->upload_date . '</td>'; echo '<td>' . ($font->is_active ? '启用' : '禁用') . '</td>'; echo '<td>'; echo '<a href="#" class="button button-small preview-font" data-font-id="' . $font->id . '">预览</a> '; echo '<a href="#" class="button button-small toggle-font" data-font-id="' . $font->id . '" data-status="' . $font->is_active . '">' . ($font->is_active ? '禁用' : '启用') . '</a> '; echo '<a href="#" class="button button-small delete-font" data-font-id="' . $font->id . '">删除</a>'; echo '</td>'; echo '</tr>'; } echo '</tbody>'; echo '</table>'; } echo '</div>'; } // 添加新字体页面 function add_new_font_page() { echo '<div class="wrap">'; echo '<h1>添加新字体</h1>'; echo '<form method="post" enctype="multipart/form-data" id="font-upload-form">'; echo '<table class="form-table">'; echo '<tr>'; echo '<th><label for="font_name">字体名称</label></th>'; echo '<td><input type="text" id="font_name" name="font_name" class="regular-text" required></td>'; echo '</tr>'; echo '<tr>'; echo '<th><label for="font_family">字体族名称</label></th>'; echo '<td><input type="text" id="font_family" name="font_family" class="regular-text" required>'; echo '<p class="description">用于CSS font-family属性的名称,如 "MyCustomFont"</p></td>'; echo '</tr>'; echo '<tr>'; echo '<th><label for="font_license">字体许可证</label></th>'; echo '<td><textarea id="font_license" name="font_license" rows="4" class="large-text"></textarea>'; echo '<p class="description">请确保您有权使用和分发此字体</p></td>'; echo '</tr>'; echo '<tr>'; echo '<th><label>字体文件</label></th>'; echo '<td>'; echo '<p><label><input type="checkbox" name="font_formats[]" value="woff2"> WOFF2格式</label>'; echo '<input type="file" name="font_file_woff2" accept=".woff2"></p>'; echo '<p><label><input type="checkbox" name="font_formats[]" value="woff"> WOFF格式</label>'; echo '<input type="file" name="font_file_woff" accept=".woff"></p>'; echo '<p><label><input type="checkbox" name="font_formats[]" value="ttf"> TTF格式</label>'; echo '<input type="file" name="font_file_ttf" accept=".ttf"></p>'; echo '</td>'; echo '</tr>'; echo '</table>'; echo '<p class="submit">'; echo '<input type="submit" name="submit_font" class="button button-primary" value="上传字体">'; echo '</p>'; echo '</form>'; echo '</div>'; // 处理字体上传 if (isset($_POST['submit_font'])) { handle_font_upload(); } } // 处理字体上传 function handle_font_upload() { if (!current_user_can('manage_options')) { wp_die('权限不足'); } global $wpdb; $table_name = $wpdb->prefix . 'custom_fonts'; $font_name = sanitize_text_field($_POST['font_name']); $font_family = sanitize_text_field($_POST['font_family']); $font_license = sanitize_textarea_field($_POST['font_license']); // 创建字体目录 $upload_dir = wp_upload_dir(); $font_dir = $upload_dir['basedir'] . '/custom-fonts/' . sanitize_title($font_family); if (!file_exists($font_dir)) { wp_mkdir_p($font_dir); } $font_data = array( 'font_name' => $font_name, 'font_family' => $font_family, 'font_license' => $font_license ); // 处理上传的文件 $formats = array('woff2', 'woff', 'ttf'); foreach ($formats as $format) { $file_key = 'font_file_' . $format; if (isset($_FILES[$file_key]) && $_FILES[$file_key]['error'] === 0) { $file = $_FILES[$file_key]; $filename = sanitize_file_name($font_family . '.' . $format); $filepath = $font_dir . '/' . $filename; if (move_uploaded_file($file['tmp_name'], $filepath)) { $font_data['font_file_' . $format] = $upload_dir['baseurl'] . '/custom-fonts/' . sanitize_title($font_family) . '/' . $filename; } } } // 保存到数据库 $wpdb->insert($table_name, $font_data); 第四部分:集成字体预览与管理功能 4.1 创建字体预览器前端界面 为了让用户能够在前端预览和管理字体,我们需要创建一个用户友好的界面: // 创建字体预览器页面模板 function font_previewer_page_template($template) { if (is_page('font-previewer')) { $new_template = locate_template(array('page-font-previewer.php')); if (!empty($new_template)) { return $new_template; } } return $template; } add_filter('template_include', 'font_previewer_page_template'); // 创建字体预览器页面内容 function create_font_previewer_page() { $page_title = '字体预览器'; $page_content = '[font_previewer]'; $page_check = get_page_by_title($page_title); if (!$page_check) { $page_data = array( 'post_title' => $page_title, 'post_content' => $page_content, 'post_status' => 'publish', 'post_type' => 'page', 'post_name' => 'font-previewer' ); wp_insert_post($page_data); } } register_activation_hook(__FILE__, 'create_font_previewer_page'); // 字体预览器短代码 function font_previewer_shortcode() { ob_start(); ?> <div class="font-previewer-container"> <div class="font-previewer-header"> <h1>在线字体预览器</h1> <p>预览和管理您的字体库,实时查看字体效果</p> </div> <div class="font-previewer-controls"> <div class="control-group"> <label for="preview-text">预览文本:</label> <input type="text" id="preview-text" value="字体的艺术在于表达" class="preview-control"> </div> <div class="control-group"> <label for="font-size">字体大小:</label> <input type="range" id="font-size" min="12" max="120" value="48" class="preview-control"> <span id="font-size-value">48px</span> </div> <div class="control-group"> <label for="font-color">字体颜色:</label> <input type="color" id="font-color" value="#333333" class="preview-control"> </div> <div class="control-group"> <label for="background-color">背景颜色:</label> <input type="color" id="background-color" value="#ffffff" class="preview-control"> </div> <div class="control-group"> <label for="font-weight">字重:</label> <select id="font-weight" class="preview-control"> <option value="300">细体 (300)</option> <option value="400" selected>常规 (400)</option> <option value="600">中等 (600)</option> <option value="700">粗体 (700)</option> <option value="900">特粗 (900)</option> </select> </div> <div class="control-group"> <label for="text-align">对齐方式:</label> <select id="text-align" class="preview-control"> <option value="left">左对齐</option> <option value="center" selected>居中</option> <option value="right">右对齐</option> <option value="justify">两端对齐</option> </select> </div> </div> <div class="font-previewer-main"> <div class="font-selector-sidebar"> <div class="font-categories"> <button class="category-btn active" data-category="all">全部字体</button> <button class="category-btn" data-category="system">系统字体</button> <button class="category-btn" data-category="google">Google字体</button> <button class="category-btn" data-category="custom">自定义字体</button> <button class="category-btn" data-category="favorites">收藏夹</button> </div> <div class="font-search"> <input type="text" id="font-search" placeholder="搜索字体..."> </div> <div class="font-list" id="font-list"> <!-- 字体列表将通过AJAX加载 --> <div class="loading-fonts">加载字体中...</div> </div> </div> <div class="font-preview-area"> <div class="preview-display" id="preview-display"> <div class="preview-text" id="preview-text-display"> 字体的艺术在于表达 </div> </div> <div class="preview-info"> <h3>字体信息</h3> <div class="info-grid"> <div class="info-item"> <span class="info-label">字体名称:</span> <span id="current-font-name">Arial</span> </div> <div class="info-item"> <span class="info-label">字体族:</span> <span id="current-font-family">Arial, sans-serif</span> </div> <div class="info-item"> <span class="info-label">字体大小:</span> <span id="current-font-size">48px</span> </div> <div class="info-item"> <span class="info-label">字重:</span> <span id="current-font-weight">400</span> </div> </div> <div class="preview-actions"> <button id="apply-font" class="button button-primary">应用此字体到网站</button> <button id="save-preset" class="button">保存为预设</button> <button id="share-preview" class="button">分享预览</button> <button id="add-to-favorites" class="button">添加到收藏夹</button> </div> <div class="css-code"> <h4>CSS代码:</h4> <pre id="css-code-output">font-family: Arial, sans-serif; font-size: 48px; font-weight: 400; color: #333333;</pre> <button id="copy-css" class="button button-small">复制CSS</button> </div> </div> </div> </div> <div class="font-presets"> <h3>字体预设</h3> <div class="presets-grid" id="presets-grid"> <!-- 预设将通过AJAX加载 --> </div> <button id="create-preset" class="button">创建新预设</button> </div> </div> <?php return ob_get_clean(); } add_shortcode('font_previewer', 'font_previewer_shortcode'); 4.2 添加AJAX功能加载字体数据 // 添加AJAX处理函数 function get_fonts_ajax() { $category = sanitize_text_field($_POST['category']); $search = sanitize_text_field($_POST['search']); $fonts = array(); // 获取系统字体 if ($category === 'all' || $category === 'system') { $system_fonts = array( array('name' => 'Arial', 'family' => 'Arial, sans-serif', 'category' => 'system'), array('name' => 'Georgia', 'family' => 'Georgia, serif', 'category' => 'system'), array('name' => 'Times New Roman', 'family' => 'Times New Roman, serif', 'category' => 'system'), array('name' => 'Verdana', 'family' => 'Verdana, sans-serif', 'category' => 'system'), array('name' => 'Courier New', 'family' => 'Courier New, monospace', 'category' => 'system'), ); $fonts = array_merge($fonts, $system_fonts); } // 获取Google字体 if ($category === 'all' || $category === 'google') { $google_fonts = array( array('name' => 'Roboto', 'family' => 'Roboto, sans-serif', 'category' => 'google'), array('name' => 'Open Sans', 'family' => 'Open Sans, sans-serif', 'category' => 'google'), array('name' => 'Montserrat', 'family' => 'Montserrat, sans-serif', 'category' => 'google'), array('name' => 'Lato', 'family' => 'Lato, sans-serif', 'category' => 'google'), array('name' => 'Poppins', 'family' => 'Poppins, sans-serif', 'category' => 'google'), array('name' => 'Source Sans Pro', 'family' => 'Source Sans Pro, sans-serif', 'category' => 'google'), array('name' => 'Oswald', 'family' => 'Oswald, sans-serif', 'category' => 'google'), array('name' => 'Raleway', 'family' => 'Raleway, sans-serif', 'category' => 'google'), ); $fonts = array_merge($fonts, $google_fonts); } // 获取自定义字体 if ($category === 'all' || $category === 'custom') { global $wpdb; $table_name = $wpdb->prefix . 'custom_fonts'; $custom_fonts = $wpdb->get_results("SELECT * FROM $table_name WHERE is_active = 1", ARRAY_A); foreach ($custom_fonts as $font) { $fonts[] = array( 'name' => $font['font_name'], 'family' => $font['font_family'], 'category' => 'custom', 'id' => $font['id'] ); } } // 应用搜索过滤 if (!empty($search)) { $fonts = array_filter($fonts, function($font) use ($search) { return stripos($font['name'], $search) !== false; }); } wp_send_json_success($fonts); } add_action('wp_ajax_get_fonts', 'get_fonts_ajax'); add_action('wp_ajax_nopriv_get_fonts', 'get_fonts_ajax'); // 保存字体预设 function save_font_preset_ajax() { if (!is_user_logged_in()) { wp_send_json_error('请先登录'); } $preset_name = sanitize_text_field($_POST['preset_name']); $font_family = sanitize_text_field($_POST['font_family']); $font_size = sanitize_text_field($_POST['font_size']); $font_color = sanitize_text_field($_POST['font_color']); $font_weight = sanitize_text_field($_POST['font_weight']); $user_id = get_current_user_id(); // 保存预设到用户meta $presets = get_user_meta($user_id, 'font_presets', true); if (empty($presets)) { $presets = array(); } $new_preset = array( 'id' => uniqid(), 'name' => $preset_name, 'font_family' => $font_family, 'font_size' => $font_size, 'font_color' => $font_color, 'font_weight' => $font_weight, 'created' => current_time('mysql') ); $presets[] = $new_preset; update_user_meta($user_id, 'font_presets', $presets); wp_send_json_success($new_preset); } add_action('wp_ajax_save_font_preset', 'save_font_preset_ajax'); // 获取字体预设 function get_font_presets_ajax() { if (!is_user_logged_in()) { wp_send_json_success(array()); } $user_id = get_current_user_id(); $presets = get_user_meta($user_id, 'font_presets', true); if (empty($presets)) { $presets = array(); } wp_send_json_success($presets); } add_action('wp_ajax_get_font_presets', 'get_font_presets_ajax'); 4.3 添加前端JavaScript交互 // 创建js/font-previewer.js文件 jQuery(document).ready(function($) { // 初始化变量 var currentFont = { family: 'Arial, sans-serif', name: 'Arial', size: '48px', color: '#333333', weight: '400', align: 'center', backgroundColor: '#ffffff' }; // 加载字体列表 function loadFonts(category, search) { $('#font-list').html('<div class="loading-fonts">加载字体中...</div>'); $.ajax({ url: fontPreviewer.ajax_url, type: 'POST', data: { action: 'get_fonts', category: category, search: search }, success: function(response) { if (response.success) { renderFontList(response.data); } } }); } // 渲染字体列表 function renderFontList(fonts) { var html = ''; if (fonts.length === 0) { html = '<div class="no-fonts">未找到匹配的字体</div>'; } else { fonts.forEach(function(font) { var isActive = font.family === currentFont.family ? 'active' : ''; html += '<div class="font-item ' + isActive + '" data-font-family="' + font.family + '" data-font-name="' + font.name + '" data-category="' + font.category + '">'; html += '<div class="font-item-preview" style="font-family: ' + font.family + '">' + font.name + '</div>'; html += '<div class="font-item-name">' + font.name + '</div>'; html += '<div class="font-item-category ' + font.category + '">' + getCategoryLabel(font.category) + '</div>'; html += '</div>'; }); } $('#font-list').html(html); } // 获取分类标签 function getCategoryLabel(category) { var labels = { 'system': '系统', 'google': 'Google', 'custom': '自定义' }; return labels[category] || category; } // 更新预览显示 function updatePreview() { var $preview = $('#preview-text-display'); $preview.css({ 'font-family': currentFont.family, 'font-size': currentFont.size, 'color': currentFont.color, 'font-weight': currentFont.weight, 'text-align': currentFont.align }); $('#preview-display').css('background-color', currentFont.backgroundColor); // 更新信息显示 $('#current-font-name').text(currentFont.name); $('#current-font-family').text(currentFont.family); $('#current-font-size').text(currentFont.size); $('#current-font-weight').text(currentFont.weight); // 更新CSS代码 updateCssCode(); } // 更新CSS代码 function updateCssCode() { var css = 'font-family: ' + currentFont.family + ';n'; css += 'font-size: ' + currentFont.size + ';n'; css += 'font-weight: ' + currentFont.weight + ';n'; css += 'color: ' + currentFont.color + ';'; if (currentFont.backgroundColor !== '#ffffff') { css += 'nbackground-color: ' + currentFont.backgroundColor + ';'; } if (currentFont.align !== 'left') { css += 'ntext-align: ' + currentFont.align + ';'; } $('#css-code-output').text(css); } // 初始化事件监听 function initEventListeners() { // 字体选择 $(document).on('click', '.font-item', function() { $('.font-item').removeClass('active'); $(this).addClass('active'); currentFont.family = $(this).data('font-family'); currentFont.name = $(this).data('font-name'); updatePreview(); }); // 分类过滤 $('.category-btn').click(function() { $('.category-btn').removeClass('active'); $(this).addClass('active'); var category = $(this).data('category'); var search = $('#font-search').val(); loadFonts(category, search); }); // 字体搜索 $('#font-search').on('input', function() { var search = $(this).val(); var category = $('.category-btn.active').data('category'); loadFonts(category, search); }); // 预览文本更改 $('#preview-text').on('input', function() { $('#preview-text-display').text($(this).val()); }); // 字体大小更改 $('#font-size').on('input', function() { var size = $(this).val() + 'px'; $('#font-size-value').text(size); currentFont.size = size; updatePreview(); }); // 字体颜色更改 $('#font-color').on('input', function() { currentFont.color = $(this).val(); updatePreview(); }); // 背景颜色更改 $('#background-color').on('input', function() { currentFont.backgroundColor = $(this).val(); updatePreview(); }); // 字重更改 $('#font-weight').change(function() { currentFont.weight = $(this).val(); updatePreview(); }); // 对齐方式更改

发表评论