手把手教程:为WordPress实现基于用户画像的个性化弹窗与内容推荐工具 引言:个性化体验在网站运营中的重要性 在当今互联网环境中,用户期望获得与其兴趣和需求高度相关的个性化体验。根据Monetate的研究,93%的企业表示个性化策略显著提升了他们的业务转化率。对于WordPress网站运营者而言,实现个性化内容推荐和智能弹窗功能,不仅能提升用户体验,还能有效提高用户参与度和转化率。 本教程将详细指导您如何通过WordPress代码二次开发,创建一个基于用户画像的个性化弹窗与内容推荐系统。我们将从零开始,逐步构建一个完整的解决方案,涵盖用户数据收集、画像构建、智能推荐算法和弹窗展示等核心功能。 第一部分:项目规划与准备工作 1.1 系统架构设计 在开始编码之前,我们需要规划整个系统的架构: 用户数据收集模块:负责收集用户行为数据 用户画像构建模块:分析用户数据并创建用户画像 内容推荐引擎:根据用户画像匹配相关内容 智能弹窗系统:在适当时机展示个性化内容 管理后台界面:供管理员配置系统参数 1.2 开发环境准备 确保您的开发环境满足以下要求: WordPress 5.0或更高版本 PHP 7.2或更高版本(推荐7.4+) MySQL 5.6或更高版本 基本的HTML/CSS/JavaScript知识 对WordPress钩子(Hooks)和过滤器(Filters)有基本了解 1.3 创建插件基础结构 首先,我们需要创建一个新的WordPress插件: 在wp-content/plugins/目录下创建新文件夹personalized-popup-recommendations 在该文件夹中创建主插件文件personalized-popup-recommendations.php 添加插件头部信息: <?php /** * Plugin Name: 个性化弹窗与内容推荐工具 * Plugin URI: https://yourwebsite.com/ * Description: 基于用户画像的个性化弹窗与内容推荐系统 * Version: 1.0.0 * Author: 您的名字 * License: GPL v2 or later * Text Domain: personalized-popup */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('PPR_VERSION', '1.0.0'); define('PPR_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('PPR_PLUGIN_URL', plugin_dir_url(__FILE__)); 第二部分:用户数据收集与存储 2.1 创建数据库表结构 我们需要创建数据库表来存储用户行为数据和画像信息。在插件激活时创建这些表: // 注册激活钩子 register_activation_hook(__FILE__, 'ppr_activate_plugin'); function ppr_activate_plugin() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_prefix = $wpdb->prefix . 'ppr_'; // 用户行为记录表 $user_behavior_table = $table_prefix . 'user_behavior'; $sql1 = "CREATE TABLE IF NOT EXISTS $user_behavior_table ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) DEFAULT NULL, session_id varchar(100) NOT NULL, behavior_type varchar(50) NOT NULL, object_id bigint(20) DEFAULT NULL, object_type varchar(50) DEFAULT NULL, metadata text, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY user_id (user_id), KEY session_id (session_id), KEY behavior_type (behavior_type) ) $charset_collate;"; // 用户画像表 $user_profile_table = $table_prefix . 'user_profiles'; $sql2 = "CREATE TABLE IF NOT EXISTS $user_profile_table ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) DEFAULT NULL, session_id varchar(100) NOT NULL, interests text, behavior_pattern text, last_updated datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY unique_user_session (user_id, session_id), KEY session_id (session_id) ) $charset_collate;"; // 推荐记录表 $recommendations_table = $table_prefix . 'recommendations'; $sql3 = "CREATE TABLE IF NOT EXISTS $recommendations_table ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) DEFAULT NULL, session_id varchar(100) NOT NULL, content_id bigint(20) NOT NULL, recommendation_type varchar(50) NOT NULL, shown_count int(11) DEFAULT 0, clicked_count int(11) DEFAULT 0, last_shown datetime DEFAULT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY user_session_content (user_id, session_id, content_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql1); dbDelta($sql2); dbDelta($sql3); // 设置默认选项 add_option('ppr_db_version', PPR_VERSION); } 2.2 实现用户行为追踪 创建用户行为追踪类,用于记录用户在网站上的各种行为: class PPR_User_Tracker { private static $instance = null; private $session_id; public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } private function __construct() { $this->init_session(); $this->setup_hooks(); } private function init_session() { if (!session_id()) { session_start(); } if (!isset($_SESSION['ppr_session_id'])) { $_SESSION['ppr_session_id'] = $this->generate_session_id(); } $this->session_id = $_SESSION['ppr_session_id']; } private function generate_session_id() { return md5(uniqid(wp_rand(), true) . $_SERVER['REMOTE_ADDR'] . time()); } private function setup_hooks() { // 追踪页面访问 add_action('wp', array($this, 'track_page_view')); // 追踪文章阅读 add_action('the_post', array($this, 'track_post_view')); // 追踪搜索行为 add_action('pre_get_posts', array($this, 'track_search')); // 追踪点击行为 add_action('wp_footer', array($this, 'add_click_tracking')); } public function track_page_view() { if (is_admin()) return; $user_id = is_user_logged_in() ? get_current_user_id() : null; $current_url = home_url($_SERVER['REQUEST_URI']); $this->record_behavior('page_view', array( 'url' => $current_url, 'page_title' => wp_get_document_title(), 'referrer' => isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '' )); } public function track_post_view($post) { if (!is_single() && !is_page()) return; $user_id = is_user_logged_in() ? get_current_user_id() : null; $this->record_behavior('post_view', $post->ID, 'post', array( 'post_type' => $post->post_type, 'category_ids' => wp_get_post_categories($post->ID, array('fields' => 'ids')), 'tag_ids' => wp_get_post_tags($post->ID, array('fields' => 'ids')) )); } public function track_search($query) { if (!$query->is_search() || $query->is_admin()) return; $search_query = get_search_query(); if (empty($search_query)) return; $user_id = is_user_logged_in() ? get_current_user_id() : null; $this->record_behavior('search', null, null, array( 'search_query' => $search_query, 'results_count' => $query->found_posts )); } public function record_behavior($behavior_type, $object_id = null, $object_type = null, $metadata = array()) { global $wpdb; $user_id = is_user_logged_in() ? get_current_user_id() : null; $table_name = $wpdb->prefix . 'ppr_user_behavior'; $wpdb->insert( $table_name, array( 'user_id' => $user_id, 'session_id' => $this->session_id, 'behavior_type' => $behavior_type, 'object_id' => $object_id, 'object_type' => $object_type, 'metadata' => $metadata ? json_encode($metadata) : null ), array('%d', '%s', '%s', '%d', '%s', '%s') ); } public function add_click_tracking() { if (is_admin()) return; ?> <script type="text/javascript"> (function($) { 'use strict'; // 追踪链接点击 $(document).on('click', 'a[href*="<?php echo home_url(); ?>"]', function(e) { var link = $(this); var href = link.attr('href'); var linkText = link.text().substring(0, 200); // 发送AJAX请求记录点击 $.ajax({ url: '<?php echo admin_url('admin-ajax.php'); ?>', type: 'POST', data: { action: 'ppr_track_click', href: href, link_text: linkText, nonce: '<?php echo wp_create_nonce('ppr_tracking_nonce'); ?>' } }); }); // 追踪按钮点击 $(document).on('click', 'button, input[type="submit"]', function(e) { var button = $(this); var buttonText = button.text().substring(0, 200) || button.val().substring(0, 200); // 发送AJAX请求记录点击 $.ajax({ url: '<?php echo admin_url('admin-ajax.php'); ?>', type: 'POST', data: { action: 'ppr_track_button_click', button_text: buttonText, nonce: '<?php echo wp_create_nonce('ppr_tracking_nonce'); ?>' } }); }); })(jQuery); </script> <?php } } // 初始化追踪器 add_action('init', function() { PPR_User_Tracker::get_instance(); }); // 处理AJAX点击追踪 add_action('wp_ajax_ppr_track_click', 'ppr_ajax_track_click'); add_action('wp_ajax_nopriv_ppr_track_click', 'ppr_ajax_track_click'); function ppr_ajax_track_click() { check_ajax_referer('ppr_tracking_nonce', 'nonce'); $tracker = PPR_User_Tracker::get_instance(); $href = sanitize_text_field($_POST['href']); $link_text = sanitize_text_field($_POST['link_text']); // 尝试从URL中提取文章ID $post_id = url_to_postid($href); $tracker->record_behavior('link_click', $post_id ?: null, $post_id ? 'post' : null, array( 'href' => $href, 'link_text' => $link_text )); wp_die(); } 第三部分:用户画像构建与分析 3.1 用户画像分析引擎 创建用户画像分析类,用于处理收集到的用户行为数据并构建用户画像: class PPR_User_Profile_Analyzer { private $user_id; private $session_id; public function __construct($user_id = null, $session_id = null) { $this->user_id = $user_id; if ($session_id) { $this->session_id = $session_id; } else { $tracker = PPR_User_Tracker::get_instance(); $this->session_id = $tracker->get_session_id(); } } public function analyze_and_update_profile() { global $wpdb; $behavior_table = $wpdb->prefix . 'ppr_user_behavior'; $profile_table = $wpdb->prefix . 'ppr_user_profiles'; // 获取用户最近的行为数据 $query = $wpdb->prepare( "SELECT * FROM $behavior_table WHERE (user_id = %d OR session_id = %s) AND created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) ORDER BY created_at DESC LIMIT 1000", $this->user_id ?: 0, $this->session_id ); $behaviors = $wpdb->get_results($query); if (empty($behaviors)) { return false; } // 分析行为数据 $interests = $this->analyze_interests($behaviors); $behavior_pattern = $this->analyze_behavior_pattern($behaviors); // 更新或插入用户画像 $wpdb->replace( $profile_table, array( 'user_id' => $this->user_id, 'session_id' => $this->session_id, 'interests' => json_encode($interests), 'behavior_pattern' => json_encode($behavior_pattern) ), array('%d', '%s', '%s', '%s') ); return true; } private function analyze_interests($behaviors) { $interests = array( 'categories' => array(), 'tags' => array(), 'post_types' => array(), 'topics' => array() ); foreach ($behaviors as $behavior) { if ($behavior->object_type === 'post' && $behavior->object_id) { $post_id = $behavior->object_id; // 分析分类兴趣 $categories = wp_get_post_categories($post_id, array('fields' => 'ids')); foreach ($categories as $cat_id) { if (!isset($interests['categories'][$cat_id])) { $interests['categories'][$cat_id] = 0; } $interests['categories'][$cat_id] += $this->get_behavior_weight($behavior->behavior_type); } // 分析标签兴趣 $tags = wp_get_post_tags($post_id, array('fields' => 'ids')); foreach ($tags as $tag_id) { if (!isset($interests['tags'][$tag_id])) { $interests['tags'][$tag_id] = 0; } $interests['tags'][$tag_id] += $this->get_behavior_weight($behavior->behavior_type); } // 分析内容类型兴趣 $post_type = get_post_type($post_id); if (!isset($interests['post_types'][$post_type])) { $interests['post_types'][$post_type] = 0; } $interests['post_types'][$post_type] += $this->get_behavior_weight($behavior->behavior_type); } // 分析搜索关键词 if ($behavior->behavior_type === 'search' && $behavior->metadata) { $metadata = json_decode($behavior->metadata, true); if (isset($metadata['search_query'])) { $keywords = $this->extract_keywords($metadata['search_query']); foreach ($keywords as $keyword) { if (!isset($interests['topics'][$keyword])) { $interests['topics'][$keyword] = 0; } $interests['topics'][$keyword] += 1; } } } } // 对兴趣进行排序,只保留前20个 foreach ($interests as $type => $items) { arsort($items); $interests[$type] = array_slice($items, 0, 20, true); } return $interests; } private function analyze_behavior_pattern($behaviors) { $pattern = array( 'visit_frequency' => 0, 'preferred_time' => array(), 'avg_session_duration' => 0, 'preferred_content_types' => array() ); // 分析访问频率 $visit_dates = array(); foreach ($behaviors as $behavior) { if ($behavior->behavior_type === 'page_view') { $date = date('Y-m-d', strtotime($behavior->created_at)); if (!in_array($date, $visit_dates)) { $visit_dates[] = $date; } } } $pattern['visit_frequency'] = count($visit_dates) / 30; // 30天内的平均每日访问次数 // 分析偏好访问时间 $hour_counts = array_fill(0, 24, 0); foreach ($behaviors as $behavior) { $hour = date('H', strtotime($behavior->created_at)); $hour_counts[(int)$hour]++; } arsort($hour_counts); $pattern['preferred_time'] = array_slice($hour_counts, 0, 3, true); return $pattern; } private function get_behavior_weight($behavior_type) { $weights = array( 'post_view' => 3, 'link_click' => 2, 'page_view' => 1, search' => 2 ); return isset($weights[$behavior_type]) ? $weights[$behavior_type] : 1; } private function extract_keywords($search_query) { // 移除停用词 $stop_words = array('的', '了', '在', '是', '我', '有', '和', '就', '不', '人', '都', '一', '一个', '上', '也', '很', '到', '说', '要', '去', '你', '会', '着', '没有', '看', '好', '自己', '这'); $words = preg_split('/[s,.。!!??]+/u', $search_query); $keywords = array(); foreach ($words as $word) { $word = trim($word); if (mb_strlen($word) > 1 && !in_array($word, $stop_words)) { $keywords[] = $word; } } return $keywords; } public function get_user_profile() { global $wpdb; $profile_table = $wpdb->prefix . 'ppr_user_profiles'; $query = $wpdb->prepare( "SELECT * FROM $profile_table WHERE (user_id = %d OR session_id = %s) ORDER BY last_updated DESC LIMIT 1", $this->user_id ?: 0, $this->session_id ); $profile = $wpdb->get_row($query); if ($profile) { $profile->interests = json_decode($profile->interests, true); $profile->behavior_pattern = json_decode($profile->behavior_pattern, true); } return $profile; } } // 定期分析用户画像add_action('ppr_daily_profile_analysis', 'ppr_perform_daily_analysis'); function ppr_perform_daily_analysis() { global $wpdb; $behavior_table = $wpdb->prefix . 'ppr_user_behavior'; // 获取最近有活动的用户和会话 $query = "SELECT DISTINCT user_id, session_id FROM $behavior_table WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)"; $users_sessions = $wpdb->get_results($query); foreach ($users_sessions as $item) { $analyzer = new PPR_User_Profile_Analyzer($item->user_id, $item->session_id); $analyzer->analyze_and_update_profile(); } } // 设置定时任务register_activation_hook(__FILE__, function() { if (!wp_next_scheduled('ppr_daily_profile_analysis')) { wp_schedule_event(time(), 'daily', 'ppr_daily_profile_analysis'); } }); register_deactivation_hook(__FILE__, function() { wp_clear_scheduled_hook('ppr_daily_profile_analysis'); }); ## 第四部分:智能内容推荐引擎 ### 4.1 推荐算法实现 创建内容推荐引擎类,根据用户画像匹配合适的内容: class PPR_Content_Recommender { private $user_profile; private $analyzer; public function __construct($user_id = null, $session_id = null) { $this->analyzer = new PPR_User_Profile_Analyzer($user_id, $session_id); $this->user_profile = $this->analyzer->get_user_profile(); } public function get_recommendations($limit = 5, $type = 'mixed') { if (!$this->user_profile) { return $this->get_fallback_recommendations($limit); } $recommendations = array(); // 基于兴趣的推荐 $interest_based = $this->get_interest_based_recommendations($limit); $recommendations = array_merge($recommendations, $interest_based); // 基于协同过滤的推荐(如果有足够数据) if (count($recommendations) < $limit) { $collaborative_based = $this->get_collaborative_recommendations($limit - count($recommendations)); $recommendations = array_merge($recommendations, $collaborative_based); } // 如果还不够,添加热门内容 if (count($recommendations) < $limit) { $popular_based = $this->get_popular_recommendations($limit - count($recommendations)); $recommendations = array_merge($recommendations, $popular_based); } // 去重并限制数量 $recommendations = $this->deduplicate_recommendations($recommendations); return array_slice($recommendations, 0, $limit); } private function get_interest_based_recommendations($limit) { $interests = $this->user_profile->interests; $recommendations = array(); // 基于分类兴趣的推荐 if (!empty($interests['categories'])) { $category_ids = array_keys($interests['categories']); $cat_recommendations = $this->get_posts_by_categories($category_ids, ceil($limit / 2)); $recommendations = array_merge($recommendations, $cat_recommendations); } // 基于标签兴趣的推荐 if (!empty($interests['tags']) && count($recommendations) < $limit) { $tag_ids = array_keys($interests['tags']); $tag_recommendations = $this->get_posts_by_tags($tag_ids, ceil($limit / 3)); $recommendations = array_merge($recommendations, $tag_recommendations); } // 基于主题关键词的推荐 if (!empty($interests['topics']) && count($recommendations) < $limit) { $topics = array_keys($interests['topics']); $topic_recommendations = $this->get_posts_by_topics($topics, ceil($limit / 4)); $recommendations = array_merge($recommendations, $topic_recommendations); } return $recommendations; } private function get_posts_by_categories($category_ids, $limit) { $args = array( 'post_type' => 'post', 'posts_per_page' => $limit * 2, // 获取更多以便筛选 'category__in' => $category_ids, 'post_status' => 'publish', 'orderby' => 'date', 'order' => 'DESC', 'date_query' => array( array( 'after' => '30 days ago' ) ) ); $posts = get_posts($args); // 根据用户兴趣权重排序 $scored_posts = array(); foreach ($posts as $post) { $score = $this->calculate_post_score($post, $category_ids); $scored_posts[] = array( 'post' => $post, 'score' => $score ); } // 按分数排序 usort($scored_posts, function($a, $b) { return $b['score'] <=> $a['score']; }); // 返回前$limit个 return array_slice(array_column($scored_posts, 'post'), 0, $limit); } private function calculate_post_score($post, $preferred_category_ids) { $score = 0; // 分类匹配分数 $post_categories = wp_get_post_categories($post->ID); $matched_categories = array_intersect($post_categories, $preferred_category_ids); $score += count($matched_categories) * 10; // 新鲜度分数(越新分数越高) $post_age = time() - strtotime($post->post_date); $freshness_score = max(0, 30 - ($post_age / (24 * 3600))); // 30天内有效 $score += $freshness_score; // 互动分数(评论数) $score += min($post->comment_count, 10); return $score; } private function get_collaborative_recommendations($limit) { global $wpdb; $behavior_table = $wpdb->prefix . 'ppr_user_behavior'; // 查找有相似行为的其他用户 $query = $wpdb->prepare( "SELECT DISTINCT b2.object_id FROM $behavior_table b1 JOIN $behavior_table b2 ON b1.object_id = b2.object_id AND b1.behavior_type = b2.behavior_type AND (b1.user_id != b2.user_id OR b1.session_id != b2.session_id) WHERE (b1.user_id = %d OR b1.session_id = %s) AND b2.object_type = 'post' AND b2.behavior_type IN ('post_view', 'link_click') AND b2.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY b2.object_id ORDER BY COUNT(*) DESC LIMIT %d", $this->user_profile->user_id ?: 0, $this->user_profile->session_id, $limit * 2 ); $post_ids = $wpdb->get_col($query); if (empty($post_ids)) { return array(); } // 获取文章详情 $args = array( 'post__in' => $post_ids, 'posts_per_page' => $limit, 'post_type' => 'post', 'post_status' => 'publish', 'orderby' => 'post__in' ); return get_posts($args); } private function get_popular_recommendations($limit) { // 获取热门文章(基于浏览量) $args = array( 'post_type' => 'post', 'posts_per_page' => $limit, 'post_status' => 'publish', 'orderby' => 'meta_value_num', 'meta_key' => 'post_views_count', 'order' => 'DESC', 'date_query' => array( array( 'after' => '30 days ago' ) ) ); return get_posts($args); } private function get_fallback_recommendations($limit) { // 对于新用户,返回最新或最受欢迎的文章 $args = array( 'post_type' => 'post', 'posts_per_page' => $limit, 'post_status' => 'publish', 'orderby' => 'date', 'order' => 'DESC' ); return get_posts($args); } private function deduplicate_recommendations($recommendations) { $seen_ids = array(); $unique_recommendations = array(); foreach ($recommendations as $post) { if (!in_array($post->ID, $seen_ids)) { $seen_ids[] = $post->ID; $unique_recommendations[] = $post; } } return $unique_recommendations; } public function record_recommendation_shown($content_id, $recommendation_type = 'popup') { global $wpdb; $table_name = $wpdb->prefix . 'ppr_recommendations'; $user_id = $this->user_profile ? $this->user_profile->user_id : null; $session_id = $this->user_profile ? $this->user_profile->session_id : PPR_User_Tracker::get_instance()->get_session_id(); // 检查是否已有记录 $existing = $wpdb->get_row($wpdb->prepare( "SELECT id, shown_count FROM $table_name WHERE content_id = %d AND (user_id = %d OR session_id = %s) AND recommendation_type = %s", $content_id, $user_id ?: 0, $session_id, $recommendation_type )); if ($existing) { // 更新现有记录 $wpdb->update( $table_name, array( 'shown_count' => $existing->shown_count + 1, 'last_shown' => current_time('mysql') ), array('id' => $existing->id), array('%d', '%s'), array('%d') ); } else { // 插入新记录 $wpdb->insert( $table_name, array( 'user_id' => $user_id, 'session_id' => $session_id, 'content_id' => $content_id, 'recommendation_type' => $recommendation_type, 'shown_count' => 1, 'last_shown' => current_time('mysql') ), array('%d', '%s', '%d', '%s', '%d', '%s') ); } } } ## 第五部分:个性化弹窗系统 ### 5.1 弹窗触发与展示逻辑 创建智能弹窗系统,根据用户行为和画像决定何时显示弹窗: class PPR_Personalized_Popup { private $recommender; private $popup_settings; public function __construct() { $this->recommender = new PPR_Content_Recommender(); $this->popup_settings = get_option('ppr_popup_settings', array()); $this->setup_hooks(); } private function setup_hooks() { // 在页面底部添加弹窗HTML add_action('wp_footer', array($this, 'render_popup_html')); // 添加弹窗样式和脚本 add_action('wp_enqueue_scripts', array($this, 'enqueue_assets')); // AJAX处理弹窗内容 add_action('wp_ajax_ppr_get_popup_content', array($this, 'ajax_get_popup_content')); add_action('wp_ajax_nopriv_ppr_get_popup_content', array($this, 'ajax_get_popup_content')); // 追踪弹窗交互 add_action('wp_ajax_ppr_track_popup_interaction', array($this, 'ajax_track_popup_interaction')); add_action('wp_ajax_nopriv_ppr_track_popup_interaction', array($this, 'ajax_track_popup_interaction')); } public function enqueue_assets() { if ($this->should_show_popup()) { wp_enqueue_style( 'ppr-popup-style', PPR_PLUGIN_URL . 'assets/css/popup.css', array(), PPR_VERSION ); wp_enqueue_script( 'ppr-popup-script', PPR_PLUGIN_URL . 'assets/js/popup.js', array('jquery'), PPR_VERSION, true ); wp_localize_script('ppr-popup-script', 'ppr_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('ppr_popup_nonce') )); } } private function should_show_popup() { // 检查是否在后台 if (is_admin()) { return false; } // 检查是否已登录且是管理员(通常不给管理员显示弹窗) if (current_user_can('manage_options')) { return false; } // 检查页面类型排除 $excluded_pages = isset($this->popup_settings['excluded_pages']) ? $this->popup_settings['excluded_pages'] : array(); if (is_page($excluded_pages) || is_single($excluded_pages)) { return false; } // 检查触发条件 $trigger_condition = isset($this->popup_settings['trigger_condition']) ? $this->popup_settings['trigger_condition'] : 'time_on_page'; switch ($trigger_condition) { case 'time_on_page': return $this->check_time_on_page_trigger(); case 'scroll_depth': return $this->check_scroll_depth_trigger(); case 'exit_intent': return $this->check_exit_intent_trigger(); default: return false; } } private function check_time_on_page_trigger() { $delay = isset($this->popup_settings['time_delay']) ? intval($this->popup_settings['time_delay']) : 30; // 这个检查将在前端JavaScript中完成 return true; } public function render_popup_html() { if (!$this->should_show_popup()) { return; } ?> <div id="ppr-personalized-popup" class="ppr-popup-container" style="display: none;"> <div class="ppr-popup-overlay"></div> <div class="ppr-popup-content"> <button class="ppr-popup-close">×</button> <div class="ppr-popup-header"> <h3>为您推荐</h3> </div> <div class="ppr-popup-body"> <div class="ppr-loading"> <div class="ppr-spinner"></div> <p>正在为您生成个性化推荐...</p> </div> <div class="ppr-recommendations-container"></div> </div> <div class="ppr-popup-footer"> <p class="ppr-privacy-notice"> 推荐基于您的浏览行为生成,<a href="<?php echo get_privacy_policy_url(); ?>">了解隐私政策</a> </p> </div> </div> </div> <?php }
发表评论分类: 应用软件
开发指南:打造网站内嵌的在线图片基础处理与美化编辑器 摘要 在当今数字化时代,视觉内容已成为网站吸引用户的关键因素。本文将详细介绍如何通过WordPress程序的代码二次开发,实现一个内嵌于网站的在线图片基础处理与美化编辑器。我们将从需求分析、技术选型、架构设计到具体实现步骤,全面解析如何将常用互联网小工具功能集成到WordPress平台中,帮助网站管理员和内容创作者在不离开网站的情况下完成图片编辑工作,提升工作效率和用户体验。 一、项目背景与需求分析 1.1 为什么需要内嵌图片编辑器? 随着内容创作需求的日益增长,网站管理员和内容创作者经常需要处理大量图片。传统的图片处理流程通常包括:下载图片→使用专业软件编辑→重新上传到网站。这个过程不仅耗时耗力,还可能导致图片质量损失。内嵌图片编辑器可以解决以下痛点: 提高工作效率:无需切换不同应用程序,直接在网站后台完成编辑 降低技术门槛:提供简单易用的界面,无需专业设计技能 保持一致性:确保所有图片符合网站风格和尺寸要求 减少存储负担:避免同一图片的多个版本占用服务器空间 1.2 功能需求分析 基于常见使用场景,我们需要实现以下核心功能: 基础编辑功能:裁剪、旋转、调整大小、翻转 色彩调整:亮度、对比度、饱和度、色相调整 滤镜效果:多种预设滤镜,支持自定义参数 添加元素:文字、形状、贴纸、水印 绘图工具:画笔、橡皮擦、形状绘制 图层管理:基本的图层操作功能 格式转换:支持常见图片格式转换 批量处理:对多张图片进行相同操作 1.3 技术可行性分析 WordPress作为全球最流行的内容管理系统,拥有强大的扩展性和丰富的API接口,为二次开发提供了良好基础。通过合理的技术选型和架构设计,完全可以实现一个功能完善的在线图片编辑器。 二、技术选型与架构设计 2.1 技术栈选择 前端技术 React.js:组件化开发,提高代码复用性和维护性 Fabric.js:强大的Canvas库,专门用于图像处理和操作 Tailwind CSS:实用优先的CSS框架,快速构建美观界面 Redux:状态管理,处理复杂编辑器状态 后端技术 PHP:WordPress原生开发语言 WordPress REST API:与前端通信,处理数据存储 ImageMagick/GD库:服务器端图像处理 MySQL:WordPress默认数据库 2.2 系统架构设计 用户界面层 (React + Fabric.js + Tailwind CSS) ↓ API通信层 (WordPress REST API + 自定义端点) ↓ 业务逻辑层 (PHP插件,处理图像操作逻辑) ↓ 数据处理层 (WordPress数据库 + 文件系统) ↓ 图像处理层 (ImageMagick/GD库 + 缓存机制) 2.3 模块划分 编辑器核心模块:基于Fabric.js的图像操作核心 工具面板模块:各种编辑工具的UI实现 文件管理模块:图片上传、保存、导出功能 滤镜效果模块:预设滤镜和自定义滤镜 文字处理模块:字体、样式、排版功能 图层管理模块:图层操作和混合模式 批量处理模块:多图片批量操作 设置与配置模块:编辑器个性化设置 三、开发环境搭建与准备工作 3.1 开发环境配置 本地WordPress环境:使用Local by Flywheel或XAMPP搭建 代码编辑器:VS Code或PHPStorm 版本控制:Git + GitHub/GitLab 调试工具:浏览器开发者工具 + WordPress调试模式 3.2 WordPress插件基础结构 创建插件目录结构: wp-image-editor/ ├── wp-image-editor.php # 主插件文件 ├── includes/ │ ├── class-editor-core.php # 编辑器核心类 │ ├── class-image-processor.php # 图像处理类 │ ├── class-file-manager.php # 文件管理类 │ └── class-api-endpoints.php # REST API端点 ├── admin/ │ ├── css/ # 后台样式 │ ├── js/ # 后台脚本 │ └── views/ # 后台视图 ├── public/ │ ├── css/ # 前台样式 │ ├── js/ # 前台脚本 │ └── views/ # 前台视图 ├── assets/ # 静态资源 │ ├── fonts/ # 字体文件 │ ├── icons/ # 图标资源 │ └── presets/ # 预设配置 └── vendor/ # 第三方库 3.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('WPIE_VERSION', '1.0.0'); define('WPIE_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('WPIE_PLUGIN_URL', plugin_dir_url(__FILE__)); define('WPIE_UPLOAD_DIR', 'wpie-editor'); // 自动加载类文件 spl_autoload_register(function ($class_name) { if (strpos($class_name, 'WPIE_') === 0) { $file = WPIE_PLUGIN_DIR . 'includes/class-' . strtolower(str_replace('_', '-', $class_name)) . '.php'; if (file_exists($file)) { require_once $file; } } }); // 初始化插件 function wpie_init() { // 检查WordPress版本 if (version_compare(get_bloginfo('version'), '5.0', '<')) { add_action('admin_notices', function() { echo '<div class="notice notice-error"><p>WordPress内嵌图片编辑器需要WordPress 5.0或更高版本。</p></div>'; }); return; } // 初始化核心类 $editor_core = new WPIE_Editor_Core(); $editor_core->init(); } add_action('plugins_loaded', 'wpie_init'); // 激活/停用插件时的操作 register_activation_hook(__FILE__, 'wpie_activate'); register_deactivation_hook(__FILE__, 'wpie_deactivate'); function wpie_activate() { // 创建必要的目录 $upload_dir = wp_upload_dir(); $wpie_dir = $upload_dir['basedir'] . '/' . WPIE_UPLOAD_DIR; if (!file_exists($wpie_dir)) { wp_mkdir_p($wpie_dir); } // 添加默认选项 add_option('wpie_settings', array( 'max_file_size' => 10, // MB 'allowed_formats' => array('jpg', 'jpeg', 'png', 'gif', 'webp'), 'default_quality' => 85, 'enable_watermark' => false, 'auto_save' => true )); } function wpie_deactivate() { // 清理临时文件 $upload_dir = wp_upload_dir(); $wpie_dir = $upload_dir['basedir'] . '/' . WPIE_UPLOAD_DIR . '/temp'; if (file_exists($wpie_dir)) { array_map('unlink', glob($wpie_dir . '/*')); rmdir($wpie_dir); } } 四、核心编辑器实现 4.1 前端编辑器框架搭建 创建React应用作为编辑器前端: // admin/js/editor-app.jsx import React, { useState, useRef, useEffect } from 'react'; import ReactDOM from 'react-dom'; import { fabric } from 'fabric'; import ToolPanel from './components/ToolPanel'; import CanvasArea from './components/CanvasArea'; import LayerPanel from './components/LayerPanel'; import PropertyPanel from './components/PropertyPanel'; import Header from './components/Header'; import { EditorProvider } from './context/EditorContext'; import './styles/editor.css'; const App = () => { const [canvas, setCanvas] = useState(null); const [activeTool, setActiveTool] = useState('select'); const [history, setHistory] = useState([]); const [historyIndex, setHistoryIndex] = useState(-1); const canvasRef = useRef(null); useEffect(() => { // 初始化Fabric.js画布 const initCanvas = new fabric.Canvas('editor-canvas', { width: 800, height: 600, backgroundColor: '#f5f5f5', preserveObjectStacking: true, }); setCanvas(initCanvas); // 保存初始状态到历史记录 saveToHistory(initCanvas); return () => { initCanvas.dispose(); }; }, []); const saveToHistory = (canvasInstance) => { const json = canvasInstance.toJSON(); const newHistory = history.slice(0, historyIndex + 1); newHistory.push(json); setHistory(newHistory); setHistoryIndex(newHistory.length - 1); }; const handleUndo = () => { if (historyIndex > 0) { const newIndex = historyIndex - 1; canvas.loadFromJSON(history[newIndex], () => { canvas.renderAll(); }); setHistoryIndex(newIndex); } }; const handleRedo = () => { if (historyIndex < history.length - 1) { const newIndex = historyIndex + 1; canvas.loadFromJSON(history[newIndex], () => { canvas.renderAll(); }); setHistoryIndex(newIndex); } }; const handleCanvasChange = () => { saveToHistory(canvas); }; return ( <EditorProvider value={{ canvas, activeTool, setActiveTool }}> <div className="editor-container"> <Header onUndo={handleUndo} onRedo={handleRedo} canUndo={historyIndex > 0} canRedo={historyIndex < history.length - 1} /> <div className="editor-main"> <ToolPanel /> <CanvasArea canvasRef={canvasRef} onCanvasChange={handleCanvasChange} /> <div className="editor-sidebar"> <PropertyPanel /> <LayerPanel /> </div> </div> </div> </EditorProvider> ); }; // 初始化编辑器 document.addEventListener('DOMContentLoaded', () => { const editorContainer = document.getElementById('wpie-editor-container'); if (editorContainer) { ReactDOM.render(<App />, editorContainer); } }); 4.2 图像处理核心类实现 // includes/class-image-processor.php class WPIE_Image_Processor { private $image_path; private $image_resource; private $image_info; public function __construct($image_path) { $this->image_path = $image_path; $this->image_info = getimagesize($image_path); $this->load_image(); } private function load_image() { $mime_type = $this->image_info['mime']; switch ($mime_type) { case 'image/jpeg': $this->image_resource = imagecreatefromjpeg($this->image_path); break; case 'image/png': $this->image_resource = imagecreatefrompng($this->image_path); break; case 'image/gif': $this->image_resource = imagecreatefromgif($this->image_path); break; case 'image/webp': if (function_exists('imagecreatefromwebp')) { $this->image_resource = imagecreatefromwebp($this->image_path); } break; default: throw new Exception('不支持的图片格式: ' . $mime_type); } if (!$this->image_resource) { throw new Exception('无法加载图片文件'); } } public function resize($width, $height, $keep_aspect_ratio = true) { $src_width = imagesx($this->image_resource); $src_height = imagesy($this->image_resource); if ($keep_aspect_ratio) { $ratio = $src_width / $src_height; if ($width / $height > $ratio) { $width = $height * $ratio; } else { $height = $width / $ratio; } } $new_image = imagecreatetruecolor($width, $height); // 保持透明度 $this->preserve_transparency($new_image); imagecopyresampled( $new_image, $this->image_resource, 0, 0, 0, 0, $width, $height, $src_width, $src_height ); imagedestroy($this->image_resource); $this->image_resource = $new_image; return $this; } public function crop($x, $y, $width, $height) { $new_image = imagecreatetruecolor($width, $height); // 保持透明度 $this->preserve_transparency($new_image); imagecopy( $new_image, $this->image_resource, 0, 0, $x, $y, $width, $height ); imagedestroy($this->image_resource); $this->image_resource = $new_image; return $this; } public function rotate($degrees) { $transparent = imagecolorallocatealpha($this->image_resource, 0, 0, 0, 127); $rotated = imagerotate($this->image_resource, $degrees, $transparent); // 保持透明度 imagesavealpha($rotated, true); imagealphablending($rotated, true); imagedestroy($this->image_resource); $this->image_resource = $rotated; return $this; } public function adjust_brightness($level) { imagefilter($this->image_resource, IMG_FILTER_BRIGHTNESS, $level); return $this; } public function adjust_contrast($level) { imagefilter($this->image_resource, IMG_FILTER_CONTRAST, $level); return $this; } public function apply_filter($filter_type, $args = []) { switch ($filter_type) { case 'grayscale': imagefilter($this->image_resource, IMG_FILTER_GRAYSCALE); break; case 'sepia': imagefilter($this->image_resource, IMG_FILTER_GRAYSCALE); imagefilter($this->image_resource, IMG_FILTER_COLORIZE, 100, 50, 0); break; case 'vintage': imagefilter($this->image_resource, IMG_FILTER_BRIGHTNESS, -30); imagefilter($this->image_resource, IMG_FILTER_CONTRAST, -10); imagefilter($this->image_resource, IMG_FILTER_COLORIZE, 60, 30, 0); break; case 'blur': $level = isset($args['level']) ? $args['level'] : 1; for ($i = 0; $i < $level; $i++) { imagefilter($this->image_resource, IMG_FILTER_GAUSSIAN_BLUR); } break; } return $this; } public function add_watermark($watermark_path, $position = 'bottom-right', $opacity = 50) { $watermark = imagecreatefrompng($watermark_path); $wm_width = imagesx($watermark); $wm_height = imagesy($watermark); $img_width = imagesx($this->image_resource); $img_height = imagesy($this->image_resource); // 计算水印位置 switch ($position) { case 'top-left': $dest_x = 10; $dest_y = 10; break; case 'top-right': $dest_x = $img_width - $wm_width - 10; $dest_y = 10; break; case 'bottom-left': $dest_x = 10; $dest_y = $img_height - $wm_height - 10; break; case 'bottom-right': $dest_x = $img_width - $wm_width - 10; $dest_y = $img_height - $wm_height - 10; break; case 'center': $dest_x = ($img_width - $wm_width) / 2; $dest_y = ($img_height - $wm_height) / 2; break; default: $dest_x = 10; $dest_y = 10; } // 合并水印 $this->imagecopymerge_alpha( $this->image_resource, $watermark, $dest_x, $dest_y, 0, 0, $wm_width, $wm_height, $opacity ); imagedestroy($watermark); return $this; } $src_x, $src_y, $src_w, $src_h, $pct ) { // 创建临时图像 $cut = imagecreatetruecolor($src_w, $src_h); // 复制源图像 imagecopy($cut, $dst_im, 0, 0, $dst_x, $dst_y, $src_w, $src_h); imagecopy($cut, $src_im, 0, 0, $src_x, $src_y, $src_w, $src_h); // 合并到目标图像 imagecopymerge($dst_im, $cut, $dst_x, $dst_y, 0, 0, $src_w, $src_h, $pct); imagedestroy($cut); } private function preserve_transparency($new_image) { $mime_type = $this->image_info['mime']; if ($mime_type == 'image/png' || $mime_type == 'image/gif') { imagealphablending($new_image, false); imagesavealpha($new_image, true); $transparent = imagecolorallocatealpha($new_image, 255, 255, 255, 127); imagefilledrectangle($new_image, 0, 0, imagesx($new_image), imagesy($new_image), $transparent); } } public function save($output_path, $quality = 85) { $extension = strtolower(pathinfo($output_path, PATHINFO_EXTENSION)); switch ($extension) { case 'jpg': case 'jpeg': imagejpeg($this->image_resource, $output_path, $quality); break; case 'png': // PNG质量参数为0-9,需要转换 $png_quality = 9 - round(($quality / 100) * 9); imagepng($this->image_resource, $output_path, $png_quality); break; case 'gif': imagegif($this->image_resource, $output_path); break; case 'webp': if (function_exists('imagewebp')) { imagewebp($this->image_resource, $output_path, $quality); } break; } return $output_path; } public function get_image_data() { ob_start(); $extension = strtolower(pathinfo($this->image_path, PATHINFO_EXTENSION)); switch ($extension) { case 'jpg': case 'jpeg': imagejpeg($this->image_resource, null, 90); break; case 'png': imagepng($this->image_resource, null, 9); break; case 'gif': imagegif($this->image_resource); break; case 'webp': if (function_exists('imagewebp')) { imagewebp($this->image_resource, null, 90); } break; } $image_data = ob_get_contents(); ob_end_clean(); return 'data:' . $this->image_info['mime'] . ';base64,' . base64_encode($image_data); } public function __destruct() { if ($this->image_resource) { imagedestroy($this->image_resource); } } } 4.3 工具面板组件实现 // admin/js/components/ToolPanel.jsx import React, { useContext } from 'react'; import { EditorContext } from '../context/EditorContext'; import { Crop, RotateCw, Type, Square, Circle, PenTool, Eraser, Image as ImageIcon, Filter, Layers, Settings } from 'lucide-react'; const ToolPanel = () => { const { activeTool, setActiveTool } = useContext(EditorContext); const tools = [ { id: 'select', name: '选择', icon: '↖' }, { id: 'crop', name: '裁剪', icon: <Crop size={20} /> }, { id: 'rotate', name: '旋转', icon: <RotateCw size={20} /> }, { id: 'text', name: '文字', icon: <Type size={20} /> }, { id: 'rectangle', name: '矩形', icon: <Square size={20} /> }, { id: 'circle', name: '圆形', icon: <Circle size={20} /> }, { id: 'brush', name: '画笔', icon: <PenTool size={20} /> }, { id: 'eraser', name: '橡皮', icon: <Eraser size={20} /> }, { id: 'image', name: '图片', icon: <ImageIcon size={20} /> }, { id: 'filter', name: '滤镜', icon: <Filter size={20} /> }, { id: 'layers', name: '图层', icon: <Layers size={20} /> }, { id: 'settings', name: '设置', icon: <Settings size={20} /> } ]; const handleToolClick = (toolId) => { setActiveTool(toolId); // 根据工具类型执行相应操作 switch (toolId) { case 'crop': // 激活裁剪模式 break; case 'text': // 添加文字对象 break; // ... 其他工具处理 } }; return ( <div className="tool-panel"> <div className="tool-category"> <h3 className="tool-category-title">选择与变换</h3> <div className="tool-grid"> {tools.slice(0, 4).map(tool => ( <button key={tool.id} className={`tool-button ${activeTool === tool.id ? 'active' : ''}`} onClick={() => handleToolClick(tool.id)} title={tool.name} > <span className="tool-icon">{tool.icon}</span> <span className="tool-name">{tool.name}</span> </button> ))} </div> </div> <div className="tool-category"> <h3 className="tool-category-title">绘图工具</h3> <div className="tool-grid"> {tools.slice(4, 8).map(tool => ( <button key={tool.id} className={`tool-button ${activeTool === tool.id ? 'active' : ''}`} onClick={() => handleToolClick(tool.id)} title={tool.name} > <span className="tool-icon">{tool.icon}</span> <span className="tool-name">{tool.name}</span> </button> ))} </div> </div> <div className="tool-category"> <h3 className="tool-category-title">效果与设置</h3> <div className="tool-grid"> {tools.slice(8).map(tool => ( <button key={tool.id} className={`tool-button ${activeTool === tool.id ? 'active' : ''}`} onClick={() => handleToolClick(tool.id)} title={tool.name} > <span className="tool-icon">{tool.icon}</span> <span className="tool-name">{tool.name}</span> </button> ))} </div> </div> </div> ); }; export default ToolPanel; 五、REST API接口设计与实现 5.1 自定义REST API端点 // includes/class-api-endpoints.php class WPIE_API_Endpoints { public function __construct() { add_action('rest_api_init', array($this, 'register_routes')); } public function register_routes() { // 上传图片 register_rest_route('wpie/v1', '/upload', array( 'methods' => 'POST', 'callback' => array($this, 'handle_upload'), 'permission_callback' => array($this, 'check_permission') )); // 处理图片 register_rest_route('wpie/v1', '/process', array( 'methods' => 'POST', 'callback' => array($this, 'handle_process'), 'permission_callback' => array($this, 'check_permission') )); // 应用滤镜 register_rest_route('wpie/v1', '/apply-filter', array( 'methods' => 'POST', 'callback' => array($this, 'apply_filter'), 'permission_callback' => array($this, 'check_permission') )); // 保存图片 register_rest_route('wpie/v1', '/save', array( 'methods' => 'POST', 'callback' => array($this, 'save_image'), 'permission_callback' => array($this, 'check_permission') )); // 批量处理 register_rest_route('wpie/v1', '/batch-process', array( 'methods' => 'POST', 'callback' => array($this, 'batch_process'), 'permission_callback' => array($this, 'check_permission') )); } public function check_permission() { return current_user_can('upload_files'); } public function handle_upload($request) { $files = $request->get_file_params(); if (empty($files['image'])) { return new WP_Error('no_file', '没有上传文件', array('status' => 400)); } $file = $files['image']; // 检查文件类型 $allowed_types = array('jpg', 'jpeg', 'png', 'gif', 'webp'); $file_ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); if (!in_array($file_ext, $allowed_types)) { return new WP_Error('invalid_type', '不支持的文件类型', array('status' => 400)); } // 检查文件大小 $max_size = get_option('wpie_settings')['max_file_size'] * 1024 * 1024; if ($file['size'] > $max_size) { return new WP_Error('file_too_large', '文件太大', array('status' => 400)); } // 创建临时目录 $upload_dir = wp_upload_dir(); $temp_dir = $upload_dir['basedir'] . '/' . WPIE_UPLOAD_DIR . '/temp'; if (!file_exists($temp_dir)) { wp_mkdir_p($temp_dir); } // 生成唯一文件名 $filename = uniqid() . '.' . $file_ext; $temp_path = $temp_dir . '/' . $filename; // 移动文件 if (move_uploaded_file($file['tmp_name'], $temp_path)) { // 创建缩略图 $thumbnail_path = $temp_dir . '/thumb_' . $filename; $this->create_thumbnail($temp_path, $thumbnail_path, 300, 300); return rest_ensure_response(array( 'success' => true, 'data' => array( 'original' => basename($temp_path), 'thumbnail' => basename($thumbnail_path), 'url' => $upload_dir['baseurl'] . '/' . WPIE_UPLOAD_DIR . '/temp/' . basename($temp_path), 'thumb_url' => $upload_dir['baseurl'] . '/' . WPIE_UPLOAD_DIR . '/temp/' . basename($thumbnail_path), 'size' => filesize($temp_path), 'dimensions' => getimagesize($temp_path) ) )); } return new WP_Error('upload_failed', '上传失败', array('status' => 500)); } private function create_thumbnail($source_path, $dest_path, $width, $height) { $processor = new WPIE_Image_Processor($source_path); $processor->resize($width, $height, true); $processor->save($dest_path, 80); } public function handle_process($request) { $params = $request->get_json_params(); if (empty($params['image']) || empty($params['operations'])) { return new WP_Error('missing_params', '缺少必要参数', array('status' => 400)); } $upload_dir = wp_upload_dir(); $image_path = $upload_dir['basedir'] . '/' . WPIE_UPLOAD_DIR . '/temp/' . $params['image']; if (!file_exists($image_path)) { return new WP_Error('file_not_found', '文件不存在', array('status' => 404)); } try { $processor = new WPIE_Image_Processor($image_path); foreach ($params['operations'] as $operation) { switch ($operation['type']) { case 'resize': $processor->resize( $operation['width'], $operation['height'], $operation['keepAspectRatio'] ?? true ); break; case 'crop': $processor->crop( $operation['x'], $operation['y'], $operation['width'], $operation['height'] ); break; case 'rotate': $processor->rotate($operation['degrees']); break; case 'brightness': $processor->adjust_brightness($operation['value']); break; case 'contrast': $processor->adjust_contrast($operation['value']); break; } } // 生成处理后的临时文件 $output_filename = 'processed_' . uniqid() . '.png'; $output_path = dirname($image_path) . '/' . $output_filename; $processor->save($output_path); return rest_ensure_response(array( 'success' => true, 'data' => array( 'filename' => $output_filename, 'url' => $upload_dir['baseurl'] . '/' . WPIE_UPLOAD_DIR . '/temp/' . $output_filename, 'data_url' => $processor->get_image_data() ) )); } catch (Exception $e) { return new WP_Error('processing_error', $e->getMessage(), array('status' => 500)); } } public function apply_filter($request) { $params = $request->get_json_params(); if (empty($params['image']) || empty($params['filter'])) { return new WP_Error('missing_params', '缺少必要参数', array('status' => 400)); } $upload_dir = wp_upload_dir(); $image_path = $upload_dir['basedir'] . '/' . WPIE_UPLOAD_DIR . '/temp/' . $params['image']; if (!file_exists($image_path)) { return new WP_Error('file_not_found', '文件不存在', array('status' => 404)); } try { $processor = new WPIE_Image_Processor($image_path); $processor->apply_filter($params['filter'], $params['args'] ?? []); $output_filename = 'filtered_' . uniqid() . '.png'; $output_path = dirname($image_path) . '/' . $output_filename; $processor->save($output_path); return rest_ensure_response(array( 'success' => true, 'data' => array( 'filename' => $output_filename, 'url' => $upload_dir['baseurl'] . '/' . WPIE_UPLOAD_DIR . '/temp/' . $output_filename, 'data_url' => $processor->get_image_data() ) )); } catch (Exception $e) { return new WP_Error('filter_error', $e->getMessage(), array('status' => 500)); } } public function save_image($request) { $params = $request->get_json_params(); if (empty($params['image']) || empty($params['filename'])) { return new WP_Error('missing_params', '缺少必要参数', array('status' => 400)); } $upload_dir = wp_upload_dir(); $temp_path = $upload_dir['basedir'] . '/' . WPIE_UPLOAD_DIR . '/temp/' . $params['image']; if (!file_exists($temp_path)) { return new WP_Error('file_not_found', '文件不存在', array('status' => 404)); } // 准备保存到媒体库 $filename = sanitize_file_name($params['filename']); $file_content = file_get_contents($temp_path); $upload = wp_upload_bits($filename, null, $file_content); if ($upload['error']) { return new WP_Error('save_failed', $upload['error'], array('status' => 500)); } // 添加到媒体库 $attachment = array( 'post_mime_type' => mime_content_type($upload['file']), 'post_title' => preg_replace('/.[^.]+$/', '', $filename), 'post_content' => '', 'post_status' => 'inherit' ); $attach_id = wp_insert_attachment($attachment, $upload['file']); if (is_wp_error($attach_id)) { return $attach_id; } // 生成元数据 require_once(ABSPATH . 'wp-admin/includes/image.php'); $attach_data = wp_generate_attachment_metadata($attach_id, $upload['file']); wp_update_attachment_metadata($attach_id, $attach_data); // 清理临时文件 unlink($temp_path); return rest_ensure_response(array( 'success' => true, 'data' => array( 'id' => $attach_id, 'url' => wp_get_attachment_url($attach_id), 'edit_url' => get_edit_post_link($attach_id, '') ) )); }
发表评论WordPress集成教程:连接公共交通API实现实时到站信息查询与展示 引言:为什么要在WordPress中集成公共交通信息? 在当今快节奏的城市生活中,实时公共交通信息已成为人们日常出行的重要参考。对于地方新闻网站、旅游博客、社区门户或企业网站而言,提供实时公交/地铁到站信息可以显著提升用户体验和网站实用性。WordPress作为全球最流行的内容管理系统,通过代码二次开发可以轻松集成这类实用功能。 本教程将详细指导您如何通过WordPress程序代码二次开发,连接公共交通API实现实时到站信息查询与展示功能。我们将从API选择、开发环境搭建、功能实现到前端展示,一步步构建一个完整的解决方案。 第一部分:准备工作与环境搭建 1.1 选择合适的公共交通API 在选择API前,需要考虑以下因素: 覆盖区域:确保API覆盖您需要的城市或地区 数据准确性:实时更新的频率和数据可靠性 成本:免费还是付费,调用限制如何 文档完整性:是否有完善的开发文档和示例 常用公共交通API推荐: TransitLand:覆盖全球多个城市的公共交通数据,提供丰富的API接口 Google Maps Transit API:集成在Google Maps平台中,数据全面但可能有使用限制 本地交通部门API:许多城市的交通部门提供官方API,数据最权威 Moovit API:提供全球范围内的公共交通数据 本教程将以一个模拟的公共交通API为例进行演示,实际应用中您需要替换为真实的API接口。 1.2 开发环境配置 在开始开发前,请确保您的WordPress环境满足以下条件: WordPress版本:5.0或更高版本 PHP版本:7.4或更高版本 必要的插件: Advanced Custom Fields(可选,用于创建管理界面) 缓存插件(如WP Rocket或W3 Total Cache) 代码编辑器:VS Code、PHPStorm或Sublime Text 本地开发环境:XAMPP、MAMP或Local by Flywheel 1.3 创建自定义插件 为了避免主题更新导致功能丢失,我们将创建一个独立插件来实现功能: <?php /** * Plugin Name: 公共交通实时信息查询 * Plugin URI: https://yourwebsite.com/ * Description: 在WordPress中集成公共交通API,显示实时到站信息 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('PT_API_PLUGIN_PATH', plugin_dir_path(__FILE__)); define('PT_API_PLUGIN_URL', plugin_dir_url(__FILE__)); define('PT_API_CACHE_TIME', 300); // 缓存时间5分钟 // 初始化插件 require_once PT_API_PLUGIN_PATH . 'includes/class-public-transport-api.php'; require_once PT_API_PLUGIN_PATH . 'includes/class-api-handler.php'; require_once PT_API_PLUGIN_PATH . 'includes/class-shortcode-handler.php'; require_once PT_API_PLUGIN_PATH . 'includes/class-admin-settings.php'; // 初始化主类 function pt_api_init() { $plugin = new Public_Transport_API(); $plugin->run(); } add_action('plugins_loaded', 'pt_api_init'); 第二部分:API连接与数据处理 2.1 创建API处理类 <?php // includes/class-api-handler.php class PT_API_Handler { private $api_key; private $api_endpoint; private $cache_enabled; public function __construct() { // 从设置中获取API配置 $options = get_option('pt_api_settings'); $this->api_key = isset($options['api_key']) ? $options['api_key'] : ''; $this->api_endpoint = isset($options['api_endpoint']) ? $options['api_endpoint'] : 'https://api.example.com/transit'; $this->cache_enabled = isset($options['enable_cache']) ? $options['enable_cache'] : true; } /** * 获取公交线路信息 */ public function get_bus_routes($city = '') { $cache_key = 'pt_bus_routes_' . md5($city); // 检查缓存 if ($this->cache_enabled) { $cached_data = get_transient($cache_key); if ($cached_data !== false) { return $cached_data; } } // 构建API请求URL $url = $this->api_endpoint . '/routes'; if (!empty($city)) { $url .= '?city=' . urlencode($city); } // 发送API请求 $response = $this->make_api_request($url); if ($response && isset($response['routes'])) { // 缓存结果 if ($this->cache_enabled) { set_transient($cache_key, $response['routes'], PT_API_CACHE_TIME); } return $response['routes']; } return false; } /** * 获取实时到站信息 */ public function get_realtime_arrivals($route_id, $stop_id) { $cache_key = 'pt_arrivals_' . md5($route_id . $stop_id); // 实时数据缓存时间较短 if ($this->cache_enabled) { $cached_data = get_transient($cache_key); if ($cached_data !== false) { return $cached_data; } } // 构建API请求URL $url = $this->api_endpoint . '/realtime/arrivals'; $url .= '?route=' . urlencode($route_id); $url .= '&stop=' . urlencode($stop_id); // 发送API请求 $response = $this->make_api_request($url); if ($response && isset($response['arrivals'])) { // 实时数据只缓存1分钟 if ($this->cache_enabled) { set_transient($cache_key, $response['arrivals'], 60); } return $response['arrivals']; } return false; } /** * 搜索公交站点 */ public function search_stops($query, $city = '') { $cache_key = 'pt_stops_search_' . md5($query . $city); if ($this->cache_enabled) { $cached_data = get_transient($cache_key); if ($cached_data !== false) { return $cached_data; } } // 构建API请求URL $url = $this->api_endpoint . '/stops/search'; $url .= '?q=' . urlencode($query); if (!empty($city)) { $url .= '&city=' . urlencode($city); } // 发送API请求 $response = $this->make_api_request($url); if ($response && isset($response['stops'])) { if ($this->cache_enabled) { set_transient($cache_key, $response['stops'], PT_API_CACHE_TIME); } return $response['stops']; } return false; } /** * 发送API请求 */ private function make_api_request($url) { // 添加API密钥 $url .= (strpos($url, '?') === false ? '?' : '&') . 'api_key=' . $this->api_key; // 设置请求参数 $args = array( 'timeout' => 15, 'redirection' => 5, 'httpversion' => '1.1', 'user-agent' => 'WordPress Public Transport Plugin/1.0', 'headers' => array( 'Accept' => 'application/json', ), ); // 发送请求 $response = wp_remote_get($url, $args); // 检查响应 if (is_wp_error($response)) { error_log('公共交通API请求错误: ' . $response->get_error_message()); return false; } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); // 检查JSON解析 if (json_last_error() !== JSON_ERROR_NONE) { error_log('公共交通API JSON解析错误: ' . json_last_error_msg()); return false; } // 检查API返回的错误 if (isset($data['error'])) { error_log('公共交通API错误: ' . $data['error']['message']); return false; } return $data; } /** * 获取支持的城市列表 */ public function get_supported_cities() { $cache_key = 'pt_supported_cities'; if ($this->cache_enabled) { $cached_data = get_transient($cache_key); if ($cached_data !== false) { return $cached_data; } } $url = $this->api_endpoint . '/cities'; $response = $this->make_api_request($url); if ($response && isset($response['cities'])) { if ($this->cache_enabled) { set_transient($cache_key, $response['cities'], 24 * HOUR_IN_SECONDS); } return $response['cities']; } // 返回默认城市列表 return array( array('id' => 'beijing', 'name' => '北京'), array('id' => 'shanghai', 'name' => '上海'), array('id' => 'guangzhou', 'name' => '广州'), array('id' => 'shenzhen', 'name' => '深圳'), ); } } 2.2 创建短代码处理器 <?php // includes/class-shortcode-handler.php class PT_Shortcode_Handler { private $api_handler; public function __construct() { $this->api_handler = new PT_API_Handler(); // 注册短代码 add_shortcode('bus_arrivals', array($this, 'render_arrivals_shortcode')); add_shortcode('bus_route_search', array($this, 'render_search_shortcode')); add_shortcode('bus_stop_info', array($this, 'render_stop_info_shortcode')); // 注册AJAX处理 add_action('wp_ajax_pt_search_stops', array($this, 'ajax_search_stops')); add_action('wp_ajax_nopriv_pt_search_stops', array($this, 'ajax_search_stops')); add_action('wp_ajax_pt_get_arrivals', array($this, 'ajax_get_arrivals')); add_action('wp_ajax_nopriv_pt_get_arrivals', array($this, 'ajax_get_arrivals')); } /** * 渲染实时到站信息短代码 */ public function render_arrivals_shortcode($atts) { // 解析短代码属性 $atts = shortcode_atts(array( 'route' => '', 'stop' => '', 'title' => '实时到站信息', 'max_results' => 5, 'show_refresh' => true, 'city' => '', ), $atts, 'bus_arrivals'); // 生成唯一ID $container_id = 'pt-arrivals-' . uniqid(); // 如果提供了route和stop,直接加载数据 $initial_data = ''; if (!empty($atts['route']) && !empty($atts['stop'])) { $arrivals = $this->api_handler->get_realtime_arrivals($atts['route'], $atts['stop']); if ($arrivals) { $initial_data = json_encode($arrivals); } } // 输出HTML ob_start(); ?> <div id="<?php echo esc_attr($container_id); ?>" class="pt-arrivals-container" data-route="<?php echo esc_attr($atts['route']); ?>" data-stop="<?php echo esc_attr($atts['stop']); ?>" data-city="<?php echo esc_attr($atts['city']); ?>" data-max-results="<?php echo esc_attr($atts['max_results']); ?>"> <div class="pt-arrivals-header"> <h3><?php echo esc_html($atts['title']); ?></h3> <?php if ($atts['show_refresh']) : ?> <button class="pt-refresh-btn" onclick="ptRefreshArrivals('<?php echo esc_js($container_id); ?>')"> <span class="dashicons dashicons-update"></span> 刷新 </button> <?php endif; ?> </div> <div class="pt-arrivals-body"> <?php if (empty($atts['route']) || empty($atts['stop'])) : ?> <div class="pt-no-selection"> <p>请选择线路和站点查看实时到站信息</p> <div class="pt-search-form"> <input type="text" class="pt-stop-search" placeholder="搜索站点..." data-target="<?php echo esc_attr($container_id); ?>"> <div class="pt-search-results"></div> </div> </div> <?php else : ?> <div class="pt-arrivals-list"> <!-- 动态加载内容 --> </div> <?php endif; ?> </div> <div class="pt-arrivals-footer"> <small>数据更新时间: <span class="pt-update-time">--:--:--</span></small> </div> </div> <?php if (!empty($initial_data)) : ?> <script> jQuery(document).ready(function($) { ptRenderArrivals('<?php echo esc_js($container_id); ?>', <?php echo $initial_data; ?>); }); </script> <?php endif; ?> <?php return ob_get_clean(); } /** * 渲染搜索短代码 */ public function render_search_shortcode($atts) { $atts = shortcode_atts(array( 'city' => '', 'placeholder' => '输入公交线路或站点名称...', ), $atts, 'bus_route_search'); $container_id = 'pt-search-' . uniqid(); ob_start(); ?> <div id="<?php echo esc_attr($container_id); ?>" class="pt-search-container"> <div class="pt-search-box"> <input type="text" class="pt-global-search" placeholder="<?php echo esc_attr($atts['placeholder']); ?>" data-city="<?php echo esc_attr($atts['city']); ?>"> <button class="pt-search-btn"> <span class="dashicons dashicons-search"></span> </button> </div> <div class="pt-search-results-container"> <div class="pt-search-results"></div> </div> </div> <?php return ob_get_clean(); } /** * AJAX搜索站点 */ public function ajax_search_stops() { // 验证nonce if (!check_ajax_referer('pt_ajax_nonce', 'nonce', false)) { wp_die('安全验证失败', 403); } $query = isset($_POST['query']) ? sanitize_text_field($_POST['query']) : ''; $city = isset($_POST['city']) ? sanitize_text_field($_POST['city']) : ''; if (empty($query)) { wp_send_json_error('请输入搜索关键词'); } $stops = $this->api_handler->search_stops($query, $city); if ($stops) { wp_send_json_success($stops); } else { wp_send_json_error('未找到相关站点'); } } /** * AJAX获取到站信息 */ public function ajax_get_arrivals() { // 验证nonce if (!check_ajax_referer('pt_ajax_nonce', 'nonce', false)) { wp_die('安全验证失败', 403); } $route_id = isset($_POST['route']) ? sanitize_text_field($_POST['route']) : ''; $stop_id = isset($_POST['stop']) ? sanitize_text_field($_POST['stop']) : ''; if (empty($route_id) || empty($stop_id)) { wp_send_json_error('缺少必要参数'); } $arrivals = $this->api_handler->get_realtime_arrivals($route_id, $stop_id); if ($arrivals) { wp_send_json_success($arrivals); } else { wp_send_json_error('获取到站信息失败'); } } } 第三部分:前端展示与用户交互 3.1 添加前端样式 /* assets/css/public-transport.css */ /* 主容器样式 */ .pt-arrivals-container { background-color: #f8f9fa; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); padding: 20px; margin: 20px 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } /* 头部样式 */ .pt-arrivals-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; border-bottom: 2px solid #007cba; padding-bottom: 10px; } .pt-arrivals-header h3 { margin: 0; color: #1d2327; font-size: 1.5em; } .pt-refresh-btn { background-color: #007cba; color: white; border: none; border-radius: 4px; padding: 8px 16px; cursor: pointer; font-size: 14px; display: flex; align-items: center; gap: 5px; transition: background-color 0.3s; } .pt-refresh-btn:hover { background-color: #005a87; } .pt-refresh-btn .dashicons { font-size: 16px; width: 16px; height: 16px; } / 到站信息列表 /.pt-arrivals-list { min-height: 200px; } .pt-arrival-item { background: white; border-radius: 6px; padding: 15px; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); transition: transform 0.2s; } .pt-arrival-item:hover { transform: translateY(-2px); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15); } .pt-arrival-info { flex: 1; } .pt-route-number { display: inline-block; background-color: #007cba; color: white; padding: 4px 12px; border-radius: 20px; font-weight: bold; font-size: 14px; margin-right: 10px; } .pt-destination { font-weight: 600; color: #1d2327; font-size: 16px; } .pt-stop-name { color: #646970; font-size: 14px; margin-top: 5px; } / 时间信息 /.pt-time-info { text-align: right; min-width: 120px; } .pt-arrival-time { font-size: 24px; font-weight: bold; color: #007cba; line-height: 1; } .pt-arrival-time.imminent { color: #d63638; } .pt-arrival-time.soon { color: #dba617; } .pt-time-unit { font-size: 12px; color: #646970; display: block; margin-top: 2px; } .pt-scheduled-time { font-size: 12px; color: #8c8f94; margin-top: 5px; } / 搜索框样式 /.pt-search-form { position: relative; margin: 20px 0; } .pt-stop-search { width: 100%; padding: 12px 15px; border: 2px solid #c3c4c7; border-radius: 6px; font-size: 16px; transition: border-color 0.3s; } .pt-stop-search:focus { outline: none; border-color: #007cba; } .pt-search-results { position: absolute; top: 100%; left: 0; right: 0; background: white; border: 1px solid #c3c4c7; border-radius: 0 0 6px 6px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); max-height: 300px; overflow-y: auto; z-index: 1000; display: none; } .pt-search-result-item { padding: 12px 15px; border-bottom: 1px solid #f0f0f1; cursor: pointer; transition: background-color 0.2s; } .pt-search-result-item:hover { background-color: #f6f7f7; } .pt-search-result-item:last-child { border-bottom: none; } .pt-result-stop-name { font-weight: 600; color: #1d2327; display: block; } .pt-result-route-list { font-size: 12px; color: #646970; margin-top: 4px; } / 全局搜索样式 /.pt-search-container { max-width: 600px; margin: 30px auto; } .pt-search-box { display: flex; gap: 10px; } .pt-global-search { flex: 1; padding: 15px; border: 2px solid #007cba; border-radius: 8px; font-size: 16px; } .pt-search-btn { background-color: #007cba; color: white; border: none; border-radius: 8px; padding: 0 25px; cursor: pointer; font-size: 16px; display: flex; align-items: center; justify-content: center; } .pt-search-btn:hover { background-color: #005a87; } / 加载状态 /.pt-loading { text-align: center; padding: 40px; color: #646970; } .pt-loading-spinner { border: 3px solid #f3f3f3; border-top: 3px solid #007cba; border-radius: 50%; width: 40px; height: 40px; animation: pt-spin 1s linear infinite; margin: 0 auto 15px; } @keyframes pt-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } / 空状态 /.pt-no-selection { text-align: center; padding: 40px 20px; color: #646970; } .pt-no-selection p { margin-bottom: 20px; font-size: 16px; } / 响应式设计 /@media (max-width: 768px) { .pt-arrival-item { flex-direction: column; align-items: flex-start; gap: 15px; } .pt-time-info { text-align: left; width: 100%; border-top: 1px solid #f0f0f1; padding-top: 10px; } .pt-search-box { flex-direction: column; } .pt-search-btn { padding: 15px; } } / 夜间模式支持 /@media (prefers-color-scheme: dark) { .pt-arrivals-container { background-color: #1d2327; color: #f0f0f1; } .pt-arrival-item { background-color: #2c3338; } .pt-arrivals-header h3, .pt-destination { color: #f0f0f1; } .pt-stop-search, .pt-global-search { background-color: #2c3338; border-color: #4f555c; color: #f0f0f1; } .pt-search-results { background-color: #2c3338; border-color: #4f555c; } } ### 3.2 添加JavaScript交互 // assets/js/public-transport.js (function($) { 'use strict'; // 全局变量 var ptAjaxUrl = ptSettings.ajax_url; var ptNonce = ptSettings.nonce; /** * 初始化插件功能 */ function initPublicTransport() { // 绑定搜索事件 $('.pt-stop-search').on('input', debounce(handleStopSearch, 300)); $('.pt-global-search').on('input', debounce(handleGlobalSearch, 300)); // 绑定搜索按钮点击事件 $('.pt-search-btn').on('click', handleSearchButtonClick); // 绑定搜索结果点击事件 $(document).on('click', '.pt-search-result-item', handleResultItemClick); // 自动刷新到站信息 initAutoRefresh(); } /** * 处理站点搜索 */ function handleStopSearch(event) { var $input = $(this); var query = $input.val().trim(); var city = $input.data('city') || ''; var $results = $input.siblings('.pt-search-results'); var targetContainer = $input.data('target'); if (query.length < 2) { $results.hide().empty(); return; } // 显示加载状态 $results.html('<div class="pt-loading"><div class="pt-loading-spinner"></div><p>搜索中...</p></div>').show(); $.ajax({ url: ptAjaxUrl, type: 'POST', data: { action: 'pt_search_stops', nonce: ptNonce, query: query, city: city }, success: function(response) { if (response.success && response.data.length > 0) { renderSearchResults($results, response.data, targetContainer); } else { $results.html('<div class="pt-no-results"><p>未找到相关站点</p></div>').show(); } }, error: function() { $results.html('<div class="pt-error"><p>搜索失败,请稍后重试</p></div>').show(); } }); } /** * 渲染搜索结果 */ function renderSearchResults($container, results, targetContainer) { var html = ''; results.forEach(function(stop) { var routes = stop.routes ? stop.routes.join('、') : '多条线路'; html += '<div class="pt-search-result-item" data-stop-id="' + stop.id + '" data-stop-name="' + stop.name + '" data-routes="' + JSON.stringify(stop.routes || []) + '" data-target="' + targetContainer + '">'; html += '<span class="pt-result-stop-name">' + stop.name + '</span>'; html += '<span class="pt-result-route-list">途经线路: ' + routes + '</span>'; html += '</div>'; }); $container.html(html).show(); } /** * 处理搜索结果点击 */ function handleResultItemClick() { var $item = $(this); var stopId = $item.data('stop-id'); var stopName = $item.data('stop-name'); var routes = $item.data('routes'); var targetContainer = $item.data('target'); // 隐藏搜索结果 $item.closest('.pt-search-results').hide().empty(); // 清空搜索框 $item.closest('.pt-search-form').find('.pt-stop-search').val(''); // 如果只有一个线路,自动选择 if (routes && routes.length === 1) { loadArrivalsForStop(targetContainer, routes[0], stopId, stopName); } else { // 显示线路选择 showRouteSelection(targetContainer, routes, stopId, stopName); } } /** * 显示线路选择 */ function showRouteSelection(containerId, routes, stopId, stopName) { var $container = $('#' + containerId); var html = '<div class="pt-route-selection">'; html += '<h4>选择线路 - ' + stopName + '</h4>'; html += '<div class="pt-route-list">'; routes.forEach(function(route) { html += '<button class="pt-route-option" data-route="' + route + '" data-stop="' + stopId + '">' + route + '路</button>'; }); html += '</div></div>'; $container.find('.pt-arrivals-body').html(html); // 绑定线路选择事件 $container.find('.pt-route-option').on('click', function() { var routeId = $(this).data('route'); loadArrivalsForStop(containerId, routeId, stopId, stopName); }); } /** * 加载到站信息 */ function loadArrivalsForStop(containerId, routeId, stopId, stopName) { var $container = $('#' + containerId); var maxResults = $container.data('max-results') || 5; // 显示加载状态 $container.find('.pt-arrivals-body').html('<div class="pt-loading"><div class="pt-loading-spinner"></div><p>加载到站信息中...</p></div>'); // 更新容器数据属性 $container.data('route', routeId); $container.data('stop', stopId); // 获取到站信息 $.ajax({ url: ptAjaxUrl, type: 'POST', data: { action: 'pt_get_arrivals', nonce: ptNonce, route: routeId, stop: stopId }, success: function(response) { if (response.success) { ptRenderArrivals(containerId, response.data); } else { showError($container, response.data || '加载失败'); } }, error: function() { showError($container, '网络错误,请稍后重试'); } }); } /** * 渲染到站信息 */ window.ptRenderArrivals = function(containerId, arrivals) { var $container = $('#' + containerId); var maxResults = $container.data('max-results') || 5; var routeId = $container.data('route'); var stopId = $container.data('stop'); if (!arrivals || arrivals.length === 0) { $container.find('.pt-arrivals-body').html('<div class="pt-no-arrivals"><p>暂无到站信息</p></div>'); updateTimeStamp($container); return; } // 限制显示数量 var displayArrivals = arrivals.slice(0, maxResults); var html = ''; displayArrivals.forEach(function(arrival) { var minutes = arrival.minutes || 0; var timeClass = 'pt-arrival-time'; if (minutes <= 2) { timeClass += ' imminent'; } else if (minutes <= 5) { timeClass += ' soon'; } html += '<div class="pt-arrival-item">'; html += '<div class="pt-arrival-info">'; html += '<span class="pt-route-number">' + (arrival.route || routeId) + '路</span>'; html += '<span class="pt-destination">' + (arrival.destination || '未知方向') + '</span>'; html += '<div class="pt-stop-name">' + (arrival.stop_name || '') + '</div>'; html += '</div>'; html += '<div class="pt-time-info">'; html += '<div class="' + timeClass + '">' + minutes + '<span class="pt-time-unit">分钟</span></div>'; if (arrival.scheduled_time) { html += '<div class="pt-scheduled-time">计划: ' + arrival.scheduled_time + '</div>'; } html += '</div>'; html += '</div>'; }); $container.find('.pt-arrivals-body').html(html); updateTimeStamp($container); }; /** * 刷新到站信息 */ window.ptRefreshArrivals = function(containerId) { var $container = $('#' + containerId); var routeId = $container.data('route'); var stopId = $container.data('stop'); if (!routeId || !stopId) { return; } loadArrivalsForStop(containerId, routeId, stopId); }; /** * 更新时间戳 */ function updateTimeStamp($container) { var now = new Date(); var timeString = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0') + ':' + now.getSeconds().toString().padStart(2, '0'); $container.find('.pt-update-time').text(timeString); } /** * 显示错误信息 */ function showError($container, message) { $container.find('.pt-arrivals-body').html('<div class="pt-error"><p>' + message + '</p></div>'); } /** * 初始化自动刷新 */ function initAutoRefresh() { // 每30秒刷新一次到站信息 setInterval(function() { $('.pt-arrivals-container').each(function() { var $container = $(this); var routeId = $container.data('route'); var stopId = $container.data('stop'); if (routeId && stopId) { // 静默刷新,不显示加载状态 $.ajax({ url: ptAjaxUrl, type: 'POST', data: { action: 'pt_get_arrivals', nonce: ptNonce, route: routeId, stop: stopId }, success: function(response) { if (response.success) { ptRenderArrivals($container.attr('id'), response.data); } } }); } }); }, 30000); // 30秒 } /** * 防抖函数 */ function debounce(func, wait) { var timeout; return function() { var context = this, args = arguments; clearTimeout(timeout); timeout = setTimeout(function() { func.apply(context, args); }, wait); }; } /** * 全局搜索处理 */ function handleGlobalSearch(event) { // 实现逻辑类似handleStopSearch,但用于全局搜索 console.log('全局搜索:', $(this).val()); } function handleSearchButtonClick
发表评论WordPress网站活动倒计时与自动提醒功能模块开发详细教程 引言:为什么需要活动倒计时与提醒功能 在当今数字营销时代,活动倒计时与自动提醒功能已成为网站提升用户参与度和转化率的重要工具。无论是电商限时促销、线上研讨会报名、产品发布会还是节日特惠活动,一个精心设计的倒计时模块能够有效制造紧迫感,激发用户行动。 对于WordPress网站管理员而言,虽然市面上有许多倒计时插件可供选择,但通过代码二次开发实现自定义功能具有独特优势:完全控制样式与行为、避免插件冲突、提升网站性能,以及根据特定需求定制功能。本教程将详细指导您如何为WordPress网站开发一个功能完整、可自定义的活动倒计时与自动提醒模块。 第一部分:开发环境准备与项目规划 1.1 开发环境配置 在开始开发之前,请确保您已准备好以下环境: 本地开发环境:推荐使用XAMPP、MAMP或Local by Flywheel搭建本地WordPress环境 代码编辑器:VS Code、Sublime Text或PHPStorm等 浏览器开发者工具:用于调试JavaScript和CSS 版本控制系统:Git(可选但推荐) 1.2 功能需求分析 我们的倒计时与提醒模块需要实现以下核心功能: 前端倒计时显示:在网站指定位置显示活动剩余时间 管理后台配置:允许管理员设置活动时间、提醒规则等 自动提醒系统:通过邮件或浏览器通知提醒用户 响应式设计:适配各种设备屏幕 短代码支持:方便在文章、页面和小工具中插入倒计时 1.3 项目文件结构规划 我们将创建一个独立的WordPress插件来实现此功能,文件结构如下: wp-content/plugins/countdown-reminder/ ├── countdown-reminder.php # 主插件文件 ├── includes/ │ ├── class-countdown-admin.php # 后台管理类 │ ├── class-countdown-frontend.php # 前端显示类 │ ├── class-countdown-reminder.php # 提醒功能类 │ └── class-countdown-shortcode.php # 短代码类 ├── assets/ │ ├── css/ │ │ ├── admin-style.css # 后台样式 │ │ └── frontend-style.css # 前端样式 │ └── js/ │ ├── admin-script.js # 后台脚本 │ └── frontend-script.js # 前端脚本 ├── templates/ # 模板文件 │ └── countdown-display.php └── languages/ # 国际化文件(可选) 第二部分:创建基础插件框架 2.1 主插件文件设置 首先创建主插件文件 countdown-reminder.php: <?php /** * Plugin Name: 活动倒计时与自动提醒 * Plugin URI: https://yourwebsite.com/countdown-reminder * Description: 为WordPress网站添加活动倒计时与自动提醒功能 * Version: 1.0.0 * Author: 您的名称 * Author URI: https://yourwebsite.com * License: GPL v2 or later * Text Domain: countdown-reminder * Domain Path: /languages */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('CDR_VERSION', '1.0.0'); define('CDR_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('CDR_PLUGIN_URL', plugin_dir_url(__FILE__)); define('CDR_PLUGIN_BASENAME', plugin_basename(__FILE__)); // 自动加载类文件 spl_autoload_register(function ($class_name) { if (strpos($class_name, 'Countdown_Reminder_') !== false) { $class_file = 'class-' . strtolower(str_replace('_', '-', $class_name)) . '.php'; $file_path = CDR_PLUGIN_DIR . 'includes/' . $class_file; if (file_exists($file_path)) { require_once $file_path; } } }); // 初始化插件 function cdr_init_plugin() { // 检查WordPress版本 if (version_compare(get_bloginfo('version'), '5.0', '<')) { add_action('admin_notices', function() { echo '<div class="notice notice-error"><p>'; echo __('活动倒计时与自动提醒插件需要WordPress 5.0或更高版本。', 'countdown-reminder'); echo '</p></div>'; }); return; } // 初始化各个功能模块 if (is_admin()) { new Countdown_Reminder_Admin(); } new Countdown_Reminder_Frontend(); new Countdown_Reminder_Shortcode(); new Countdown_Reminder_Reminder(); } add_action('plugins_loaded', 'cdr_init_plugin'); // 插件激活时执行的操作 function cdr_activate_plugin() { // 创建必要的数据库表 cdr_create_database_tables(); // 设置默认选项 $default_options = array( 'default_countdown_style' => 'modern', 'enable_email_reminders' => true, 'enable_browser_notifications' => false, 'reminder_intervals' => array('24', '6', '1'), // 小时 'default_timezone' => get_option('timezone_string', 'UTC'), ); add_option('cdr_settings', $default_options); // 添加定时任务 if (!wp_next_scheduled('cdr_daily_reminder_check')) { wp_schedule_event(time(), 'hourly', 'cdr_daily_reminder_check'); } } register_activation_hook(__FILE__, 'cdr_activate_plugin'); // 插件停用时执行的操作 function cdr_deactivate_plugin() { // 清除定时任务 wp_clear_scheduled_hook('cdr_daily_reminder_check'); } register_deactivation_hook(__FILE__, 'cdr_deactivate_plugin'); // 创建数据库表 function cdr_create_database_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'cdr_events'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, event_name varchar(255) NOT NULL, event_description text, start_datetime datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, end_datetime datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, timezone varchar(50) DEFAULT 'UTC', countdown_style varchar(50) DEFAULT 'modern', enable_reminders tinyint(1) DEFAULT 1, reminder_intervals varchar(255) DEFAULT '24,6,1', status varchar(20) DEFAULT 'active', created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 创建提醒记录表 $reminders_table = $wpdb->prefix . 'cdr_reminders'; $sql2 = "CREATE TABLE IF NOT EXISTS $reminders_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, event_id mediumint(9) NOT NULL, user_id mediumint(9), user_email varchar(255), reminder_type varchar(20) DEFAULT 'email', reminder_sent tinyint(1) DEFAULT 0, scheduled_time datetime NOT NULL, sent_time datetime, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY event_id (event_id), KEY user_email (user_email) ) $charset_collate;"; dbDelta($sql2); } 第三部分:开发后台管理界面 3.1 创建管理类 在 includes/class-countdown-admin.php 中创建后台管理类: <?php class Countdown_Reminder_Admin { private $settings_page; public function __construct() { add_action('admin_menu', array($this, 'add_admin_menu')); add_action('admin_init', array($this, 'register_settings')); add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts')); add_action('wp_ajax_cdr_save_event', array($this, 'ajax_save_event')); add_action('wp_ajax_cdr_get_events', array($this, 'ajax_get_events')); add_action('wp_ajax_cdr_delete_event', array($this, 'ajax_delete_event')); } public function add_admin_menu() { $this->settings_page = add_menu_page( __('活动倒计时管理', 'countdown-reminder'), __('活动倒计时', 'countdown-reminder'), 'manage_options', 'countdown-reminder', array($this, 'render_admin_page'), 'dashicons-clock', 30 ); // 添加子菜单 add_submenu_page( 'countdown-reminder', __('添加新活动', 'countdown-reminder'), __('添加新活动', 'countdown-reminder'), 'manage_options', 'countdown-reminder-add', array($this, 'render_add_event_page') ); add_submenu_page( 'countdown-reminder', __('设置', 'countdown-reminder'), __('设置', 'countdown-reminder'), 'manage_options', 'countdown-reminder-settings', array($this, 'render_settings_page') ); } public function enqueue_admin_scripts($hook) { if (strpos($hook, 'countdown-reminder') === false) { return; } // 引入日期时间选择器 wp_enqueue_style('jquery-ui-style', 'https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css'); wp_enqueue_script('jquery-ui-datepicker'); // 引入时间选择器 wp_enqueue_style('timepicker-style', 'https://cdnjs.cloudflare.com/ajax/libs/timepicker/1.3.5/jquery.timepicker.min.css'); wp_enqueue_script('timepicker-script', 'https://cdnjs.cloudflare.com/ajax/libs/timepicker/1.3.5/jquery.timepicker.min.js', array('jquery'), '1.3.5', true); // 引入插件自定义样式和脚本 wp_enqueue_style('cdr-admin-style', CDR_PLUGIN_URL . 'assets/css/admin-style.css', array(), CDR_VERSION); wp_enqueue_script('cdr-admin-script', CDR_PLUGIN_URL . 'assets/js/admin-script.js', array('jquery', 'jquery-ui-datepicker'), CDR_VERSION, true); // 本地化脚本 wp_localize_script('cdr-admin-script', 'cdr_admin_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('cdr_admin_nonce'), 'confirm_delete' => __('确定要删除这个活动吗?', 'countdown-reminder'), 'saving' => __('保存中...', 'countdown-reminder'), 'saved' => __('已保存', 'countdown-reminder') )); } public function render_admin_page() { ?> <div class="wrap cdr-admin-wrap"> <h1><?php _e('活动倒计时管理', 'countdown-reminder'); ?></h1> <div class="cdr-admin-header"> <a href="<?php echo admin_url('admin.php?page=countdown-reminder-add'); ?>" class="button button-primary"> <?php _e('添加新活动', 'countdown-reminder'); ?> </a> <a href="<?php echo admin_url('admin.php?page=countdown-reminder-settings'); ?>" class="button"> <?php _e('设置', 'countdown-reminder'); ?> </a> </div> <div class="cdr-events-list"> <h2><?php _e('活动列表', 'countdown-reminder'); ?></h2> <div class="cdr-events-table-container"> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <th><?php _e('活动名称', 'countdown-reminder'); ?></th> <th><?php _e('开始时间', 'countdown-reminder'); ?></th> <th><?php _e('结束时间', 'countdown-reminder'); ?></th> <th><?php _e('状态', 'countdown-reminder'); ?></th> <th><?php _e('操作', 'countdown-reminder'); ?></th> </tr> </thead> <tbody id="cdr-events-body"> <!-- 通过AJAX加载活动列表 --> <tr> <td colspan="5" class="cdr-loading"> <?php _e('加载中...', 'countdown-reminder'); ?> </td> </tr> </tbody> </table> </div> </div> <div class="cdr-shortcode-help"> <h3><?php _e('使用短代码', 'countdown-reminder'); ?></h3> <p><?php _e('在文章或页面中使用以下短代码显示倒计时:', 'countdown-reminder'); ?></p> <code>[countdown event_id="1"]</code> <p><?php _e('或使用活动名称:', 'countdown-reminder'); ?></p> <code>[countdown event_name="新年促销"]</code> <p><?php _e('在小工具中使用文本小工具并插入短代码。', 'countdown-reminder'); ?></p> </div> </div> <?php } public function render_add_event_page() { $timezones = timezone_identifiers_list(); $current_timezone = get_option('timezone_string', 'UTC'); ?> <div class="wrap cdr-admin-wrap"> <h1><?php _e('添加新活动', 'countdown-reminder'); ?></h1> <form id="cdr-event-form" method="post"> <?php wp_nonce_field('cdr_save_event_nonce', 'cdr_nonce'); ?> <div class="cdr-form-section"> <h2><?php _e('活动基本信息', 'countdown-reminder'); ?></h2> <div class="cdr-form-row"> <label for="event_name"><?php _e('活动名称 *', 'countdown-reminder'); ?></label> <input type="text" id="event_name" name="event_name" required class="regular-text"> </div> <div class="cdr-form-row"> <label for="event_description"><?php _e('活动描述', 'countdown-reminder'); ?></label> <textarea id="event_description" name="event_description" rows="4" class="large-text"></textarea> </div> </div> <div class="cdr-form-section"> <h2><?php _e('时间设置', 'countdown-reminder'); ?></h2> <div class="cdr-form-row"> <label for="start_date"><?php _e('开始日期', 'countdown-reminder'); ?></label> <input type="text" id="start_date" name="start_date" class="cdr-datepicker"> <input type="text" id="start_time" name="start_time" class="cdr-timepicker" placeholder="HH:MM"> </div> <div class="cdr-form-row"> <label for="end_date"><?php _e('结束日期 *', 'countdown-reminder'); ?></label> <input type="text" id="end_date" name="end_date" class="cdr-datepicker" required> <input type="text" id="end_time" name="end_time" class="cdr-timepicker" placeholder="HH:MM" required> </div> <div class="cdr-form-row"> <label for="timezone"><?php _e('时区', 'countdown-reminder'); ?></label> <select id="timezone" name="timezone"> <?php foreach ($timezones as $tz): ?> <option value="<?php echo esc_attr($tz); ?>" <?php selected($current_timezone, $tz); ?>> <?php echo esc_html($tz); ?> </option> <?php endforeach; ?> </select> </div> </div> <div class="cdr-form-section"> <h2><?php _e('倒计时样式', 'countdown-reminder'); ?></h2> <div class="cdr-form-row"> <label for="countdown_style"><?php _e('选择样式', 'countdown-reminder'); ?></label> <select id="countdown_style" name="countdown_style"> <option value="modern"><?php _e('现代风格', 'countdown-reminder'); ?></option> <option value="classic"><?php _e('经典风格', 'countdown-reminder'); ?></option> <option value="minimal"><?php _e('简约风格', 'countdown-reminder'); ?></option> <option value="flip"><?php _e('翻牌效果', 'countdown-reminder'); ?></option> </select> </div> <div class="cdr-style-previews"> preview" data-style="modern"> <h4><?php _e('现代风格', 'countdown-reminder'); ?></h4> <div class="preview-countdown"> <div class="countdown-unit"> <span class="number">12</span> <span class="label"><?php _e('天', 'countdown-reminder'); ?></span> </div> <div class="countdown-unit"> <span class="number">23</span> <span class="label"><?php _e('小时', 'countdown-reminder'); ?></span> </div> <div class="countdown-unit"> <span class="number">45</span> <span class="label"><?php _e('分钟', 'countdown-reminder'); ?></span> </div> <div class="countdown-unit"> <span class="number">30</span> <span class="label"><?php _e('秒', 'countdown-reminder'); ?></span> </div> </div> </div> </div> </div> <div class="cdr-form-section"> <h2><?php _e('提醒设置', 'countdown-reminder'); ?></h2> <div class="cdr-form-row"> <label> <input type="checkbox" id="enable_reminders" name="enable_reminders" value="1" checked> <?php _e('启用自动提醒', 'countdown-reminder'); ?> </label> </div> <div class="cdr-form-row cdr-reminder-intervals" id="reminder_intervals_container"> <label><?php _e('提醒间隔(活动开始前的小时数)', 'countdown-reminder'); ?></label> <div class="interval-checkboxes"> <label><input type="checkbox" name="reminder_intervals[]" value="168" checked> <?php _e('7天前', 'countdown-reminder'); ?></label> <label><input type="checkbox" name="reminder_intervals[]" value="72" checked> <?php _e('3天前', 'countdown-reminder'); ?></label> <label><input type="checkbox" name="reminder_intervals[]" value="24" checked> <?php _e('1天前', 'countdown-reminder'); ?></label> <label><input type="checkbox" name="reminder_intervals[]" value="6"> <?php _e('6小时前', 'countdown-reminder'); ?></label> <label><input type="checkbox" name="reminder_intervals[]" value="1"> <?php _e('1小时前', 'countdown-reminder'); ?></label> <label><input type="checkbox" name="reminder_intervals[]" value="0.5"> <?php _e('30分钟前', 'countdown-reminder'); ?></label> </div> </div> <div class="cdr-form-row"> <label for="reminder_message"><?php _e('自定义提醒消息', 'countdown-reminder'); ?></label> <textarea id="reminder_message" name="reminder_message" rows="3" class="large-text" placeholder="<?php esc_attr_e('例如:您关注的活动即将开始!不要错过...', 'countdown-reminder'); ?>"></textarea> <p class="description"><?php _e('使用 {event_name}、{start_time}、{days_left} 等占位符', 'countdown-reminder'); ?></p> </div> </div> <div class="cdr-form-actions"> <button type="submit" class="button button-primary button-large"> <?php _e('保存活动', 'countdown-reminder'); ?> </button> <a href="<?php echo admin_url('admin.php?page=countdown-reminder'); ?>" class="button button-large"> <?php _e('返回列表', 'countdown-reminder'); ?> </a> </div> <div id="cdr-form-message" class="cdr-message" style="display:none;"></div> </form> </div> <?php } public function render_settings_page() { $settings = get_option('cdr_settings', array()); ?> <div class="wrap cdr-admin-wrap"> <h1><?php _e('倒计时与提醒设置', 'countdown-reminder'); ?></h1> <form method="post" action="options.php"> <?php settings_fields('cdr_settings_group'); ?> <?php do_settings_sections('cdr_settings_group'); ?> <div class="cdr-form-section"> <h2><?php _e('常规设置', 'countdown-reminder'); ?></h2> <div class="cdr-form-row"> <label for="default_timezone"><?php _e('默认时区', 'countdown-reminder'); ?></label> <select id="default_timezone" name="cdr_settings[default_timezone]"> <?php $timezones = timezone_identifiers_list(); $current_tz = isset($settings['default_timezone']) ? $settings['default_timezone'] : get_option('timezone_string', 'UTC'); foreach ($timezones as $tz) { echo '<option value="' . esc_attr($tz) . '" ' . selected($current_tz, $tz, false) . '>' . esc_html($tz) . '</option>'; } ?> </select> </div> <div class="cdr-form-row"> <label for="default_countdown_style"><?php _e('默认倒计时样式', 'countdown-reminder'); ?></label> <select id="default_countdown_style" name="cdr_settings[default_countdown_style]"> <option value="modern" <?php selected(isset($settings['default_countdown_style']) ? $settings['default_countdown_style'] : 'modern', 'modern'); ?>> <?php _e('现代风格', 'countdown-reminder'); ?> </option> <option value="classic" <?php selected(isset($settings['default_countdown_style']) ? $settings['default_countdown_style'] : '', 'classic'); ?>> <?php _e('经典风格', 'countdown-reminder'); ?> </option> <option value="minimal" <?php selected(isset($settings['default_countdown_style']) ? $settings['default_countdown_style'] : '', 'minimal'); ?>> <?php _e('简约风格', 'countdown-reminder'); ?> </option> <option value="flip" <?php selected(isset($settings['default_countdown_style']) ? $settings['default_countdown_style'] : '', 'flip'); ?>> <?php _e('翻牌效果', 'countdown-reminder'); ?> </option> </select> </div> </div> <div class="cdr-form-section"> <h2><?php _e('提醒设置', 'countdown-reminder'); ?></h2> <div class="cdr-form-row"> <label> <input type="checkbox" name="cdr_settings[enable_email_reminders]" value="1" <?php checked(isset($settings['enable_email_reminders']) ? $settings['enable_email_reminders'] : true, true); ?>> <?php _e('启用邮件提醒', 'countdown-reminder'); ?> </label> </div> <div class="cdr-form-row"> <label> <input type="checkbox" name="cdr_settings[enable_browser_notifications]" value="1" <?php checked(isset($settings['enable_browser_notifications']) ? $settings['enable_browser_notifications'] : false, true); ?>> <?php _e('启用浏览器通知', 'countdown-reminder'); ?> </label> <p class="description"><?php _e('注意:浏览器通知需要用户授权', 'countdown-reminder'); ?></p> </div> <div class="cdr-form-row"> <label for="reminder_intervals"><?php _e('默认提醒间隔(小时,用逗号分隔)', 'countdown-reminder'); ?></label> <input type="text" id="reminder_intervals" name="cdr_settings[reminder_intervals]" value="<?php echo isset($settings['reminder_intervals']) ? esc_attr(is_array($settings['reminder_intervals']) ? implode(',', $settings['reminder_intervals']) : $settings['reminder_intervals']) : '24,6,1'; ?>" class="regular-text"> <p class="description"><?php _e('例如:24,6,1 表示活动开始前24小时、6小时和1小时发送提醒', 'countdown-reminder'); ?></p> </div> <div class="cdr-form-row"> <label for="email_from_name"><?php _e('发件人名称', 'countdown-reminder'); ?></label> <input type="text" id="email_from_name" name="cdr_settings[email_from_name]" value="<?php echo isset($settings['email_from_name']) ? esc_attr($settings['email_from_name']) : get_bloginfo('name'); ?>" class="regular-text"> </div> <div class="cdr-form-row"> <label for="email_from_address"><?php _e('发件人邮箱', 'countdown-reminder'); ?></label> <input type="email" id="email_from_address" name="cdr_settings[email_from_address]" value="<?php echo isset($settings['email_from_address']) ? esc_attr($settings['email_from_address']) : get_bloginfo('admin_email'); ?>" class="regular-text"> </div> <div class="cdr-form-row"> <label for="default_reminder_subject"><?php _e('默认提醒邮件主题', 'countdown-reminder'); ?></label> <input type="text" id="default_reminder_subject" name="cdr_settings[default_reminder_subject]" value="<?php echo isset($settings['default_reminder_subject']) ? esc_attr($settings['default_reminder_subject']) : __('活动提醒: {event_name}', 'countdown-reminder'); ?>" class="large-text"> </div> <div class="cdr-form-row"> <label for="default_reminder_message"><?php _e('默认提醒邮件内容', 'countdown-reminder'); ?></label> <textarea id="default_reminder_message" name="cdr_settings[default_reminder_message]" rows="6" class="large-text"><?php echo isset($settings['default_reminder_message']) ? esc_textarea($settings['default_reminder_message']) : __('亲爱的用户, 您关注的活动 "{event_name}" 即将开始!开始时间: {start_time}剩余时间: {days_left} 天 {hours_left} 小时 点击这里查看详情: {event_url} 祝好,{site_name}', 'countdown-reminder'); ?></textarea> <p class="description"><?php _e('可用占位符: {event_name}, {start_time}, {end_time}, {days_left}, {hours_left}, {event_url}, {site_name}', 'countdown-reminder'); ?></p> </div> </div> <div class="cdr-form-section"> <h2><?php _e('显示设置', 'countdown-reminder'); ?></h2> <div class="cdr-form-row"> <label> <input type="checkbox" name="cdr_settings[show_past_events]" value="1" <?php checked(isset($settings['show_past_events']) ? $settings['show_past_events'] : false, true); ?>> <?php _e('显示已结束的活动', 'countdown-reminder'); ?> </label> </div> <div class="cdr-form-row"> <label> <input type="checkbox" name="cdr_settings[auto_refresh]" value="1" <?php checked(isset($settings['auto_refresh']) ? $settings['auto_refresh'] : true, true); ?>> <?php _e('自动刷新倒计时', 'countdown-reminder'); ?> </label> </div> <div class="cdr-form-row"> <label for="refresh_interval"><?php _e('刷新间隔(秒)', 'countdown-reminder'); ?></label> <input type="number" id="refresh_interval" name="cdr_settings[refresh_interval]" value="<?php echo isset($settings['refresh_interval']) ? esc_attr($settings['refresh_interval']) : '1'; ?>" min="1" max="60" class="small-text"> </div> </div> <?php submit_button(); ?> </form> </div> <?php } public function register_settings() { register_setting('cdr_settings_group', 'cdr_settings', array($this, 'sanitize_settings')); } public function sanitize_settings($input) { $sanitized = array(); if (isset($input['default_timezone'])) { $sanitized['default_timezone'] = sanitize_text_field($input['default_timezone']); } if (isset($input['default_countdown_style'])) { $sanitized['default_countdown_style'] = sanitize_text_field($input['default_countdown_style']); } if (isset($input['enable_email_reminders'])) { $sanitized['enable_email_reminders'] = (bool)$input['enable_email_reminders']; } if (isset($input['enable_browser_notifications'])) { $sanitized['enable_browser_notifications'] = (bool)$input['enable_browser_notifications']; } if (isset($input['reminder_intervals'])) { $intervals = explode(',', $input['reminder_intervals']); $sanitized_intervals = array(); foreach ($intervals as $interval) { $clean_interval = absint(trim($interval)); if ($clean_interval > 0) { $sanitized_intervals[] = $clean_interval; } } $sanitized['reminder_intervals'] = $sanitized_intervals; } if (isset($input['email_from_name'])) { $sanitized['email_from_name'] = sanitize_text_field($input['email_from_name']); } if (isset($input['email_from_address'])) { $sanitized['email_from_address'] = sanitize_email($input['email_from_address']); } if (isset($input['default_reminder_subject'])) { $sanitized['default_reminder_subject'] = sanitize_text_field($input['default_reminder_subject']); } if (isset($input['default_reminder_message'])) { $sanitized['default_reminder_message'] = wp_kses_post($input['default_reminder_message']); } if (isset($input['show_past_events'])) { $sanitized['show_past_events'] = (bool)$input['show_past_events']; } if (isset($input['auto_refresh'])) { $sanitized['auto_refresh'] = (bool)$input['auto_refresh']; } if (isset($input['refresh_interval'])) { $sanitized['refresh_interval'] = absint($input['refresh_interval']); if ($sanitized['refresh_interval'] < 1) $sanitized['refresh_interval'] = 1; if ($sanitized['refresh_interval'] > 60) $sanitized['refresh_interval'] = 60; } return $sanitized; } public function ajax_save_event() { // 验证nonce if (!check_ajax_referer('cdr_admin_nonce', 'nonce', false)) { wp_die(json_encode(array( 'success' => false, 'message' => __('安全验证失败', 'countdown-reminder') ))); } // 验证权限 if (!current_user_can('manage_options')) { wp_die(json_encode(array( 'success' => false, 'message' => __('权限不足', 'countdown-reminder') ))); } global $wpdb; $table_name = $wpdb->prefix . 'cdr_events'; // 获取并清理数据 $event_data = array( 'event_name' => sanitize_text_field($_POST['event_name']), 'event_description' => wp_kses_post($_POST['event_description']), 'timezone' => sanitize_text_field($_POST['timezone']), 'countdown_style' => sanitize_text_field($_POST['countdown_style']), 'enable_reminders' => isset($_POST['enable_reminders']) ? 1 : 0, 'status' => 'active' ); // 处理日期时间 $start_date = sanitize_text_field($_POST['start_date']); $start_time = sanitize_text_field($_POST['start_time']); $end_date = sanitize_text_field($_POST['end_date']); $end_time = sanitize_text_field($_POST['end_time']); if (!empty($start_date) && !empty($start_time)) { $event_data['start_datetime'] = $this->combine_datetime($start_date, $start_time, $event_data['timezone']); } if (!empty($end_date) && !empty($end_time)) { $event_data['end_datetime'] = $this->combine_datetime($end_date, $end_time, $event
发表评论一步步教你,集成在线字体库预览与个性化网络字库管理工具到WordPress网站 引言:为什么网站需要字体管理工具? 在当今数字化时代,网站设计已成为品牌形象和用户体验的重要组成部分。字体作为视觉传达的核心元素之一,直接影响着网站的可读性、美观性和品牌一致性。然而,大多数网站开发者面临一个共同挑战:如何在保持设计灵活性的同时,确保字体加载效率和跨平台兼容性? 传统的字体管理方法通常依赖于系统默认字体或有限的网络字体库,这限制了设计师的创意表达。随着在线字体库的蓬勃发展,如Google Fonts、Adobe Fonts等提供了数千种高质量字体,但如何高效集成和管理这些资源成为了新的技术难题。 本文将详细介绍如何通过WordPress代码二次开发,集成在线字体库预览与个性化网络字库管理工具,使您的网站拥有专业级的字体管理能力,同时提升用户体验和设计自由度。 第一部分:准备工作与环境配置 1.1 理解WordPress字体管理现状 在开始开发之前,我们需要了解WordPress默认的字体处理方式。WordPress核心本身不提供高级字体管理功能,但通过主题和插件可以实现基本的字体控制。常见的方法包括: 主题自定义器中的字体选择器 通过CSS直接引入字体文件 使用插件如"Easy Google Fonts"或"Use Any Font" 然而,这些方法存在局限性:缺乏实时预览、字体库有限、管理不够直观等。我们的目标是创建一个更强大、更灵活的解决方案。 1.2 开发环境搭建 为了安全地进行代码二次开发,建议按照以下步骤配置开发环境: 创建子主题:避免直接修改父主题,确保更新不会丢失自定义功能 /* Theme Name: My Custom Theme Template: parent-theme-folder-name */ 启用调试模式:在wp-config.php中设置开发模式 define('WP_DEBUG', true); define('WP_DEBUG_LOG', true); define('WP_DEBUG_DISPLAY', false); 安装必要的开发工具:代码编辑器(如VS Code)、本地服务器环境(如XAMPP或Local by Flywheel)、Git版本控制系统 备份现有网站:在进行任何代码修改前,完整备份网站文件和数据库 1.3 确定技术架构 我们的字体管理工具将采用以下技术架构: 前端:HTML5、CSS3、JavaScript(使用Vue.js或React简化交互开发) 后端:PHP(WordPress核心语言) 数据存储:WordPress自定义数据库表 + 选项API API集成:连接Google Fonts API和其他字体服务API 缓存机制:Transients API提高性能 第二部分:创建字体管理核心功能 2.1 设计数据库结构 我们需要创建自定义数据库表来存储用户字体配置和收藏。在主题的functions.php中添加以下代码: function create_font_management_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'custom_fonts'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, font_name varchar(255) NOT NULL, font_family varchar(255) NOT NULL, font_source varchar(100) DEFAULT 'google', font_variants text, font_subsets text, is_active tinyint(1) DEFAULT 1, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 创建字体使用记录表 $usage_table = $wpdb->prefix . 'font_usage'; $sql_usage = "CREATE TABLE IF NOT EXISTS $usage_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, font_id mediumint(9) NOT NULL, element_type varchar(100), element_selector varchar(255), user_id bigint(20), created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), FOREIGN KEY (font_id) REFERENCES $table_name(id) ON DELETE CASCADE ) $charset_collate;"; dbDelta($sql_usage); } add_action('after_setup_theme', 'create_font_management_tables'); 2.2 集成Google Fonts API Google Fonts提供了丰富的免费字体库和易于使用的API。我们将创建一个类来处理与Google Fonts API的交互: class Google_Fonts_Integration { private $api_key = ''; // 可选,如果需要更高API限制 private $api_url = 'https://www.googleapis.com/webfonts/v1/webfonts'; public function __construct() { // 初始化方法 } public function get_all_fonts($sort = 'popularity') { $transient_key = 'google_fonts_list_' . $sort; $fonts = get_transient($transient_key); if (false === $fonts) { $url = $this->api_url . '?sort=' . $sort; if (!empty($this->api_key)) { $url .= '&key=' . $this->api_key; } $response = wp_remote_get($url); if (is_wp_error($response)) { return false; } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if (isset($data['items'])) { $fonts = $data['items']; set_transient($transient_key, $fonts, WEEK_IN_SECONDS); } } return $fonts; } public function generate_font_import_url($font_family, $variants = array(), $subsets = array()) { $base_url = 'https://fonts.googleapis.com/css2'; $family_param = urlencode($font_family); if (!empty($variants)) { $variant_str = implode(',', $variants); $family_param .= ':wght@' . $variant_str; } $query_args = array('family' => $family_param); if (!empty($subsets)) { $query_args['subset'] = implode(',', $subsets); } return add_query_arg($query_args, $base_url); } } 2.3 创建字体预览功能 实时预览是字体管理工具的核心功能。我们将创建一个交互式预览界面: function enqueue_font_preview_assets() { // 仅在需要字体管理的页面加载资源 if (is_admin() || current_user_can('edit_theme_options')) { wp_enqueue_style('font-preview-admin', get_template_directory_uri() . '/css/font-preview.css'); wp_enqueue_script('vue-js', 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js', array(), '2.6.14', true); wp_enqueue_script('font-preview-app', get_template_directory_uri() . '/js/font-preview.js', array('vue-js', 'jquery'), '1.0.0', true); // 传递数据到JavaScript wp_localize_script('font-preview-app', 'fontPreviewData', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('font_preview_nonce'), 'default_text' => __('The quick brown fox jumps over the lazy dog', 'textdomain'), 'font_categories' => array('serif', 'sans-serif', 'display', 'handwriting', 'monospace') )); } } add_action('admin_enqueue_scripts', 'enqueue_font_preview_assets'); 创建预览界面的HTML结构: <div id="font-preview-app" class="font-management-wrapper"> <div class="font-preview-container"> <div class="font-controls"> <div class="font-search"> <input type="text" v-model="searchQuery" placeholder="搜索字体..."> <select v-model="selectedCategory"> <option value="">所有分类</option> <option v-for="category in categories" :value="category">{{ category }}</option> </select> <select v-model="selectedSort"> <option value="popularity">最受欢迎</option> <option value="trending">趋势</option> <option value="alpha">字母顺序</option> </select> </div> <div class="preview-controls"> <div class="text-input"> <textarea v-model="previewText" placeholder="输入预览文本"></textarea> </div> <div class="size-control"> <label>字体大小:</label> <input type="range" v-model="fontSize" min="12" max="72" step="1"> <span>{{ fontSize }}px</span> </div> </div> </div> <div class="font-grid"> <div v-for="font in filteredFonts" :key="font.family" class="font-card" :class="{ active: isFontActive(font.family) }"> <div class="font-preview" :style="{ fontFamily: font.family, fontSize: fontSize + 'px' }"> {{ previewText || defaultText }} </div> <div class="font-info"> <h3>{{ font.family }}</h3> <div class="font-actions"> <button @click="toggleFont(font)" class="button"> {{ isFontActive(font.family) ? '已启用' : '启用' }} </button> <button @click="showVariants(font)" class="button button-secondary"> 变体 </button> </div> </div> </div> </div> <div class="pagination" v-if="totalPages > 1"> <button @click="prevPage" :disabled="currentPage === 1">上一页</button> <span>第 {{ currentPage }} 页,共 {{ totalPages }} 页</span> <button @click="nextPage" :disabled="currentPage === totalPages">下一页</button> </div> </div> <!-- 字体变体选择模态框 --> <div v-if="showVariantModal" class="modal-overlay"> <div class="modal-content"> <h2>选择字体变体: {{ selectedFont.family }}</h2> <div class="variant-grid"> <div v-for="variant in selectedFont.variants" :key="variant" class="variant-option"> <input type="checkbox" :id="'variant-' + variant" :value="variant" v-model="selectedVariants"> <label :for="'variant-' + variant" :style="{ fontFamily: selectedFont.family, fontWeight: variant }"> {{ getVariantName(variant) }} </label> </div> </div> <div class="modal-actions"> <button @click="applyVariants" class="button button-primary">应用</button> <button @click="closeModal" class="button">取消</button> </div> </div> </div> </div> 第三部分:实现个性化字体管理 3.1 创建字体管理界面 在WordPress后台添加自定义菜单页面,用于管理字体: function add_font_management_page() { add_menu_page( '字体管理', '字体管理', 'manage_options', 'font-management', 'render_font_management_page', 'dashicons-editor-textcolor', 30 ); add_submenu_page( 'font-management', '我的字体库', '我的字体库', 'manage_options', 'font-library', 'render_font_library_page' ); add_submenu_page( 'font-management', '字体设置', '字体设置', 'manage_options', 'font-settings', 'render_font_settings_page' ); } add_action('admin_menu', 'add_font_management_page'); function render_font_management_page() { ?> <div class="wrap"> <h1><?php echo esc_html(get_admin_page_title()); ?></h1> <div id="font-management-app"> <!-- 这里将加载Vue.js应用 --> </div> </div> <?php } 3.2 实现AJAX字体处理 处理字体启用/禁用、保存配置等操作: // 处理字体启用/禁用 add_action('wp_ajax_toggle_font', 'handle_toggle_font'); function handle_toggle_font() { // 验证nonce if (!wp_verify_nonce($_POST['nonce'], 'font_preview_nonce')) { wp_die('安全验证失败'); } $font_family = sanitize_text_field($_POST['font_family']); $variants = isset($_POST['variants']) ? array_map('sanitize_text_field', $_POST['variants']) : array('regular'); $action = sanitize_text_field($_POST['action_type']); // 'enable' 或 'disable' global $wpdb; $table_name = $wpdb->prefix . 'custom_fonts'; if ($action === 'enable') { // 检查是否已存在 $existing = $wpdb->get_var($wpdb->prepare( "SELECT id FROM $table_name WHERE font_family = %s", $font_family )); if (!$existing) { $wpdb->insert( $table_name, array( 'font_name' => $font_family, 'font_family' => $font_family, 'font_variants' => json_encode($variants), 'is_active' => 1 ), array('%s', '%s', '%s', '%d') ); } else { $wpdb->update( $table_name, array('is_active' => 1, 'font_variants' => json_encode($variants)), array('id' => $existing), array('%d', '%s'), array('%d') ); } // 更新CSS文件 update_font_css_file(); wp_send_json_success(array('message' => '字体已启用')); } else { $wpdb->update( $table_name, array('is_active' => 0), array('font_family' => $font_family), array('%d'), array('%s') ); update_font_css_file(); wp_send_json_success(array('message' => '字体已禁用')); } } // 更新字体CSS文件 function update_font_css_file() { global $wpdb; $table_name = $wpdb->prefix . 'custom_fonts'; $active_fonts = $wpdb->get_results( "SELECT font_family, font_variants, font_source FROM $table_name WHERE is_active = 1" ); $css_content = "/* 动态生成的字体CSS - 最后更新: " . date('Y-m-d H:i:s') . " */nn"; foreach ($active_fonts as $font) { $variants = json_decode($font->font_variants, true); if ($font->font_source === 'google') { $font_integration = new Google_Fonts_Integration(); $import_url = $font_integration->generate_font_import_url($font->font_family, $variants); $css_content .= "@import url('" . esc_url($import_url) . "');n"; } // 为每个字体生成CSS类 $font_class = sanitize_title($font->font_family); $css_content .= ".font-" . $font_class . " { font-family: '" . $font->font_family . "', sans-serif; }n"; } // 保存到文件 $upload_dir = wp_upload_dir(); $font_css_path = $upload_dir['basedir'] . '/dynamic-fonts.css'; file_put_contents($font_css_path, $css_content); // 更新选项记录文件路径 update_option('dynamic_font_css_path', $upload_dir['baseurl'] . '/dynamic-fonts.css'); } 3.3 集成自定义字体上传 除了在线字体库,用户可能希望上传自己的字体文件: function handle_custom_font_upload() { if (!current_user_can('upload_files')) { wp_die('权限不足'); } if (!wp_verify_nonce($_POST['_wpnonce'], 'custom_font_upload')) { wp_die('安全验证失败'); } $font_file = $_FILES['font_file']; // 检查文件类型 $allowed_types = array('ttf', 'otf', 'woff', 'woff2', 'eot'); $file_ext = pathinfo($font_file['name'], PATHINFO_EXTENSION); if (!in_array(strtolower($file_ext), $allowed_types)) { wp_send_json_error(array('message' => '不支持的文件类型')); } // 处理上传 require_once(ABSPATH . 'wp-admin/includes/file.php'); $upload_overrides = array('test_form' => false); $movefile = wp_handle_upload($font_file, $upload_overrides); if ($movefile && !isset($movefile['error'])) { // 保存字体信息到数据库 $font_name = sanitize_text_field($_POST['font_name']); $font_family = !empty($_POST['font_family']) ? sanitize_text_field($_POST['font_family']) : $font_name; global $wpdb; $table_name = $wpdb->prefix . 'custom_fonts'; $wpdb->insert( $table_name, array( 'font_name' => $font_name, 'font_family' => $font_family, 'font_source' => 'custom', 'font_variants' => json_encode(array('regular', 'bold')), 'font_file_url' => $movefile['url'], 'is_active' => 1 ), array('%s', '%s', '%s', '%s', '%s', '%d') ); // 生成@font-face规则 generate_custom_fontface_css($font_family, $movefile['url'], $file_ext); wp_send_json_success(array( 'message' => '字体上传成功', 'font_family' => $font_family, 'file_url' => $movefile['url'] )); } else { wp_send_json_error(array('message' => $movefile['error'])); } } add_action('wp_ajax_upload_custom_font', 'handle_custom_font_upload'); function generate_custom_fontface_css($font_family, $font_url, $font_ext) { $css_content = "n/* 自定义字体: " . $font_family . " */n"; $css_content .= "@font-face {n"; $css_content .= " font-family: '" . $font_family . "';n"; $css_content .= " src: url('" . $font_url . "') format('" . get_font_format($font_ext) . "');n"; $css_content .= " font-weight: normal;n"; $css_content .= " font-style: normal;n"; $css_content .= " font-display: swap;n"; $css_content .= "}nn"; // 追加到动态字体CSS文件 $upload_dir = wp_upload_dir(); $font_css_path = $upload_dir['basedir'] . '/dynamic-fonts.css'; if (file_exists($font_css_path)) { file_put_contents($font_css_path, $css_content, FILE_APPEND); } } function get_font_format($extension) { $formats = array( 'ttf' => 'truetype', 'otf' => 'opentype', 'woff' => 'woff', 'woff2' => 'woff2', 'eot' => 'embedded-opentype' ); return isset($formats[strtolower($extension)]) ? $formats[strtolower($extension)] : 'truetype'; } 第四部分:优化字体加载与性能 4.1 实现字体加载优化策略 字体加载是影响网站性能的关键因素。我们将实施多种优化策略: class Font_Performance_Optimizer { public function __construct() { add_action('wp_head', array($this, 'add_font_preload_tags'), 1); add_action('wp_enqueue_scripts', array($this, 'optimize_font_loading')); } public function add_font_preload_tags() { $active_fonts = $this->get_critical_fonts(); foreach ($active_fonts as $font) { if ($font['source'] === 'google') { // 预加载关键字体文件 echo '<link rel="preload" as="style" href="' . esc_url($font['url']) . '" crossorigin>'; // 添加预连接到Google Fonts域名 echo '<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>'; } elseif ($font['source'] === 'custom') { echo '<link rel="preload" as="font" href="' . esc_url($font['url']) . '" type="font/' . esc_attr($font['format']) . '" crossorigin>'; } } } public function optimize_font_loading() { // 延迟加载非关键字体 $font_css_url = get_option('dynamic_font_css_path'); if ($font_css_url) { // 使用media="print"技巧延迟加载 wp_register_style('dynamic-fonts-print', $font_css_url, array(), null, 'print'); wp_enqueue_style('dynamic-fonts-print'); // 加载后切换到all媒体类型 wp_add_inline_script('jquery', ' (function() { var fontStylesheet = document.querySelector("link[media='print']"); if (fontStylesheet) { fontStylesheet.media = "all"; } })(); '); } } private function get_critical_fonts() { global $wpdb; $table_name = $wpdb->prefix . 'custom_fonts'; $fonts = $wpdb->get_results( "SELECT font_family, font_source, font_file_url FROM $table_name WHERE is_active = 1 AND is_critical = 1", ARRAY_A ); return $fonts; } // 实现字体缓存机制 public static function cache_font_files($font_url, $font_family) { $upload_dir = wp_upload_dir(); $cache_dir = $upload_dir['basedir'] . '/font-cache/'; // 创建缓存目录 if (!file_exists($cache_dir)) { wp_mkdir_p($cache_dir); } $cache_key = md5($font_url . $font_family); $cache_file = $cache_dir . $cache_key . '.css'; // 检查缓存是否有效(24小时) if (file_exists($cache_file) && (time() - filemtime($cache_file)) < DAY_IN_SECONDS) { return str_replace($upload_dir['basedir'], $upload_dir['baseurl'], $cache_file); } // 下载并缓存字体CSS $response = wp_remote_get($font_url); if (!is_wp_error($response)) { $font_css = wp_remote_retrieve_body($response); // 本地化字体文件URL $font_css = self::localize_font_urls($font_css, $font_family); file_put_contents($cache_file, $font_css); return str_replace($upload_dir['basedir'], $upload_dir['baseurl'], $cache_file); } return $font_url; // 失败时返回原始URL } private static function localize_font_urls($css_content, $font_family) { // 匹配字体URL并下载到本地 preg_match_all('/url((https?://[^)]+))/', $css_content, $matches); if (!empty($matches[1])) { $upload_dir = wp_upload_dir(); $font_dir = $upload_dir['basedir'] . '/fonts/' . sanitize_title($font_family) . '/'; if (!file_exists($font_dir)) { wp_mkdir_p($font_dir); } foreach ($matches[1] as $font_url) { $font_filename = basename(parse_url($font_url, PHP_URL_PATH)); $local_font_path = $font_dir . $font_filename; // 下载字体文件 if (!file_exists($local_font_path)) { $font_data = wp_remote_get($font_url); if (!is_wp_error($font_data)) { file_put_contents($local_font_path, wp_remote_retrieve_body($font_data)); } } // 替换URL为本地路径 $local_font_url = str_replace( $upload_dir['basedir'], $upload_dir['baseurl'], $local_font_path ); $css_content = str_replace($font_url, $local_font_url, $css_content); } } return $css_content; } } new Font_Performance_Optimizer(); 4.2 实现字体使用分析 了解字体使用情况有助于优化决策: class Font_Analytics { public function __construct() { add_action('wp_footer', array($this, 'track_font_usage')); add_action('admin_menu', array($this, 'add_analytics_page')); } public function track_font_usage() { if (!current_user_can('manage_options')) { return; } ?> <script> (function() { // 检测页面中使用的字体 var usedFonts = new Set(); // 检查所有元素的计算字体 var allElements = document.querySelectorAll('*'); allElements.forEach(function(el) { var computedStyle = window.getComputedStyle(el); var fontFamily = computedStyle.fontFamily; if (fontFamily && fontFamily !== 'inherit') { // 提取字体族名称(去除引号和备用字体) var fonts = fontFamily.split(',')[0].replace(/['"]/g, '').trim(); if (fonts) { usedFonts.add(fonts); } } }); // 发送使用数据到服务器 if (usedFonts.size > 0) { var data = { action: 'track_font_usage', fonts: Array.from(usedFonts), page_url: window.location.href, nonce: '<?php echo wp_create_nonce("font_analytics_nonce"); ?>' }; // 使用navigator.sendBeacon确保数据发送 if (navigator.sendBeacon) { var formData = new FormData(); for (var key in data) { formData.append(key, data[key]); } navigator.sendBeacon('<?php echo admin_url("admin-ajax.php"); ?>', formData); } } })(); </script> <?php } public static function handle_usage_tracking() { if (!wp_verify_nonce($_POST['nonce'], 'font_analytics_nonce')) { wp_die('安全验证失败'); } $fonts = isset($_POST['fonts']) ? array_map('sanitize_text_field', $_POST['fonts']) : array(); $page_url = sanitize_url($_POST['page_url']); $user_id = get_current_user_id(); global $wpdb; $usage_table = $wpdb->prefix . 'font_usage'; $fonts_table = $wpdb->prefix . 'custom_fonts'; foreach ($fonts as $font_family) { // 查找字体ID $font_id = $wpdb->get_var($wpdb->prepare( "SELECT id FROM $fonts_table WHERE font_family = %s", $font_family )); if ($font_id) { // 记录使用情况 $wpdb->insert( $usage_table, array( 'font_id' => $font_id, 'page_url' => $page_url, 'user_id' => $user_id ), array('%d', '%s', '%d') ); } } wp_die(); } public function add_analytics_page() { add_submenu_page( 'font-management', '字体分析', '字体分析', 'manage_options', 'font-analytics', array($this, 'render_analytics_page') ); } public function render_analytics_page() { ?> <div class="wrap"> <h1>字体使用分析</h1> <div class="analytics-container"> <div class="analytics-stats"> <?php $this->display_font_statistics(); ?> </div> <div class="analytics-charts"> <canvas id="fontUsageChart" width="400" height="200"></canvas> </div> <div class="analytics-table"> <?php $this->display_font_usage_table(); ?> </div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script> jQuery(document).ready(function($) { var ctx = document.getElementById('fontUsageChart').getContext('2d'); var chart = new Chart(ctx, { type: 'bar', data: { labels: <?php echo json_encode($this->get_font_labels()); ?>, datasets: [{ label: '使用次数', data: <?php echo json_encode($this->get_font_usage_data()); ?>, backgroundColor: 'rgba(54, 162, 235, 0.5)', borderColor: 'rgba(54, 162, 235, 1)', borderWidth: 1 }] }, options: { responsive: true, scales: { y: { beginAtZero: true } } } }); }); </script> <?php } private function display_font_statistics() { global $wpdb; $fonts_table = $wpdb->prefix . 'custom_fonts'; $usage_table = $wpdb->prefix . 'font_usage'; $total_fonts = $wpdb->get_var("SELECT COUNT(*) FROM $fonts_table WHERE is_active = 1"); $total_usage = $wpdb->get_var("SELECT COUNT(*) FROM $usage_table"); $most_used = $wpdb->get_row( "SELECT f.font_family, COUNT(u.id) as usage_count FROM $usage_table u JOIN $fonts_table f ON u.font_id = f.id GROUP BY u.font_id ORDER BY usage_count DESC LIMIT 1" ); echo '<div class="stat-card">'; echo '<h3>活跃字体数量</h3>'; echo '<p class="stat-number">' . $total_fonts . '</p>'; echo '</div>'; echo '<div class="stat-card">'; echo '<h3>总使用次数</h3>'; echo '<p class="stat-number">' . $total_usage . '</p>'; echo '</div>'; if ($most_used) { echo '<div class="stat-card">'; echo '<h3>最常用字体</h3>'; echo '<p class="stat-number">' . esc_html($most_used->font_family) . '</p>'; echo '<p class="stat-desc">使用次数: ' . $most_used->usage_count . '</p>'; echo '</div>'; } } } add_action('wp_ajax_track_font_usage', array('Font_Analytics', 'handle_usage_tracking')); add_action('wp_ajax_nopriv_track_font_usage', array('Font_Analytics', 'handle_usage_tracking')); 第五部分:创建前端字体选择器组件 5.1 开发可视化字体选择器 为内容编辑器和前端用户提供字体选择功能: class Frontend_Font_Selector { public function __construct() { // 为古腾堡编辑器添加字体选择控件 add_action('enqueue_block_editor_assets', array($this, 'add_block_editor_font_controls')); // 为经典编辑器添加字体下拉菜单 add_filter('mce_buttons_2', array($this, 'add_font_select_to_tinymce')); add_filter('tiny_mce_before_init', array($this, 'customize_tinymce_fonts')); // 前端字体选择器短代码 add_shortcode('font_selector', array($this, 'render_font_selector_shortcode')); } public function add_block_editor_font_controls() { wp_enqueue_script( 'gutenberg-font-controls', get_template_directory_uri() . '/js/gutenberg-font-controls.js', array('wp-blocks', 'wp-element', 'wp-components', 'wp-editor', 'wp-data'), '1.0.0', true ); // 传递可用字体列表 $active_fonts = $this->get_active_fonts(); wp_localize_script('gutenberg-font-controls', 'fontControlsData', array( 'fonts' => $active_fonts, 'defaultFont' => get_theme_mod('primary_font', 'Arial, sans-serif') )); // 添加编辑器样式 $font_css_url = get_option('dynamic_font_css_path'); if ($font_css_url) { wp_enqueue_style('editor-fonts', $font_css_url); } } public function add_font_select_to_tinymce($buttons) { array_unshift($buttons, 'fontselect'); return $buttons; } public function customize_tinymce_fonts($init) { $active_fonts = $this->get_active_fonts(); $font_formats = ''; foreach ($active_fonts as $font) { $font_formats .= $font['name'] . '=' . $font['family'] . ';'; } // 添加默认字体 $font_formats .= 'Arial=arial,helvetica,sans-serif;'; $font_formats .= 'Times New Roman=times new roman,times,serif;'; $init['font_formats'] = $font_formats; $init['fontsize_formats'] = "8px 10px 12px 14px 16px 18px 20px 24px 28px 32px 36px 48px"; return $init; } public function render_font_selector_shortcode($atts) { $atts = shortcode_atts(array( 'type' => 'dropdown', // dropdown, preview, or inline 'category' => '',
发表评论WordPress插件开发教程:实现网站文章自动转语音并生成播客订阅 引言:为什么需要文章转语音功能? 在当今快节奏的数字时代,用户获取信息的方式日益多样化。虽然阅读文字内容仍然是主要方式,但越来越多的人开始通过音频内容获取信息——在通勤途中、做家务时、运动时,音频内容提供了无需视觉参与的便利。根据Edison Research的数据,2023年有超过1亿美国人每月收听播客,这一数字比五年前增长了近一倍。 对于WordPress网站所有者而言,将文章内容转换为音频格式具有多重优势: 提高内容可访问性,服务视觉障碍用户 增加用户停留时间,降低跳出率 拓展内容分发渠道,触及更广泛的受众 提升SEO表现,增加网站可见性 创造新的变现机会,如播客广告 本教程将详细指导您开发一个完整的WordPress插件,实现文章自动转语音并生成播客订阅功能。我们将从零开始,逐步构建这个功能强大的工具。 第一部分:开发环境准备与插件基础结构 1.1 开发环境配置 在开始插件开发前,确保您已准备好以下环境: 本地开发环境:推荐使用XAMPP、MAMP或Local by Flywheel WordPress安装:最新版本的WordPress(建议5.8+) 代码编辑器:VS Code、PHPStorm或Sublime Text PHP版本:7.4或更高版本 调试工具:安装Query Monitor和Debug Bar插件 1.2 创建插件基础文件结构 首先,在WordPress的wp-content/plugins/目录下创建一个新文件夹,命名为article-to-podcast。在该文件夹中创建以下基础文件: article-to-podcast/ ├── article-to-podcast.php # 主插件文件 ├── uninstall.php # 卸载脚本 ├── includes/ # 核心功能文件 │ ├── class-tts-engine.php # 文字转语音引擎 │ ├── class-podcast-feed.php # 播客Feed生成 │ ├── class-admin-ui.php # 管理界面 │ └── class-ajax-handler.php # AJAX处理 ├── assets/ # 静态资源 │ ├── css/ │ ├── js/ │ └── images/ ├── languages/ # 国际化文件 └── templates/ # 前端模板 1.3 编写插件主文件 打开article-to-podcast.php,添加以下代码作为插件头部信息: <?php /** * Plugin Name: Article to Podcast Converter * Plugin URI: https://yourwebsite.com/article-to-podcast * Description: 自动将WordPress文章转换为语音并生成播客订阅 * Version: 1.0.0 * Author: Your Name * Author URI: https://yourwebsite.com * License: GPL v2 or later * Text Domain: article-to-podcast * Domain Path: /languages */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('ATPC_VERSION', '1.0.0'); define('ATPC_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('ATPC_PLUGIN_URL', plugin_dir_url(__FILE__)); define('ATPC_PLUGIN_BASENAME', plugin_basename(__FILE__)); // 自动加载类文件 spl_autoload_register(function ($class) { $prefix = 'ATPC_'; $base_dir = ATPC_PLUGIN_DIR . 'includes/'; $len = strlen($prefix); if (strncmp($prefix, $class, $len) !== 0) { return; } $relative_class = substr($class, $len); $file = $base_dir . 'class-' . str_replace('_', '-', strtolower($relative_class)) . '.php'; if (file_exists($file)) { require $file; } }); // 初始化插件 function atpc_init() { // 检查必要扩展 if (!extension_loaded('simplexml')) { add_action('admin_notices', function() { echo '<div class="notice notice-error"><p>'; echo __('Article to Podcast插件需要SimpleXML扩展。请联系您的主机提供商启用此扩展。', 'article-to-podcast'); echo '</p></div>'; }); return; } // 初始化核心类 $tts_engine = new ATPC_TTS_Engine(); $podcast_feed = new ATPC_Podcast_Feed(); $admin_ui = new ATPC_Admin_UI(); $ajax_handler = new ATPC_Ajax_Handler(); // 注册激活/停用钩子 register_activation_hook(__FILE__, ['ATPC_Admin_UI', 'activate_plugin']); register_deactivation_hook(__FILE__, ['ATPC_Admin_UI', 'deactivate_plugin']); // 加载文本域 load_plugin_textdomain('article-to-podcast', false, dirname(ATPC_PLUGIN_BASENAME) . '/languages'); } add_action('plugins_loaded', 'atpc_init'); 第二部分:文字转语音引擎实现 2.1 选择TTS(文字转语音)服务 目前市场上有多种TTS服务可供选择,每种都有其优缺点: Google Cloud Text-to-Speech:质量高,支持多种语言,但需要付费 Amazon Polly:自然语音,价格合理,有免费套餐 Microsoft Azure Cognitive Services:语音自然度高,支持情感表达 IBM Watson Text to Speech:企业级解决方案 本地解决方案:如eSpeak(免费但质量较低) 本教程将使用Amazon Polly作为示例,因为它提供每月500万字符的免费套餐,适合中小型网站。 2.2 实现TTS引擎类 创建includes/class-tts-engine.php文件: <?php class ATPC_TTS_Engine { private $aws_access_key; private $aws_secret_key; private $aws_region; private $polly_client; public function __construct() { $options = get_option('atpc_settings'); $this->aws_access_key = isset($options['aws_access_key']) ? $options['aws_access_key'] : ''; $this->aws_secret_key = isset($options['aws_secret_key']) ? $options['aws_secret_key'] : ''; $this->aws_region = isset($options['aws_region']) ? $options['aws_region'] : 'us-east-1'; // 初始化AWS Polly客户端 $this->init_polly_client(); // 添加文章保存钩子 add_action('save_post', [$this, 'generate_audio_on_save'], 10, 3); } private function init_polly_client() { if (empty($this->aws_access_key) || empty($this->aws_secret_key)) { return; } try { require_once ATPC_PLUGIN_DIR . 'vendor/autoload.php'; $this->polly_client = new AwsPollyPollyClient([ 'version' => 'latest', 'region' => $this->aws_region, 'credentials' => [ 'key' => $this->aws_access_key, 'secret' => $this->aws_secret_key ] ]); } catch (Exception $e) { error_log('ATPC: Failed to initialize Polly client - ' . $e->getMessage()); } } public function generate_audio_on_save($post_id, $post, $update) { // 检查是否自动生成音频 $auto_generate = get_option('atpc_auto_generate', 'yes'); if ($auto_generate !== 'yes') { return; } // 检查文章状态和类型 if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) { return; } $allowed_post_types = get_option('atpc_post_types', ['post']); if (!in_array($post->post_type, $allowed_post_types)) { return; } // 检查文章是否已发布 if ($post->post_status !== 'publish') { return; } // 生成音频 $this->generate_audio($post_id); } public function generate_audio($post_id) { $post = get_post($post_id); if (!$post) { return false; } // 获取文章内容 $content = $this->prepare_content($post); // 检查内容长度 if (strlen($content) < 50) { error_log('ATPC: Content too short for post ID ' . $post_id); return false; } // 生成音频文件 $audio_url = $this->synthesize_speech($content, $post_id); if ($audio_url) { // 保存音频信息到文章元数据 update_post_meta($post_id, '_atpc_audio_url', $audio_url); update_post_meta($post_id, '_atpc_audio_generated', current_time('mysql')); update_post_meta($post_id, '_atpc_audio_duration', $this->calculate_duration($content)); // 触发动作,可供其他插件使用 do_action('atpc_audio_generated', $post_id, $audio_url); return $audio_url; } return false; } private function prepare_content($post) { // 获取文章标题和内容 $title = $post->post_title; $content = $post->post_content; // 移除短代码 $content = strip_shortcodes($content); // 移除HTML标签,但保留段落结构 $content = wp_strip_all_tags($content); // 清理多余空格和换行 $content = preg_replace('/s+/', ' ', $content); // 添加标题 $full_content = sprintf(__('文章标题:%s。正文内容:%s', 'article-to-podcast'), $title, $content); // 限制长度(Polly限制为3000个字符) if (strlen($full_content) > 3000) { $full_content = substr($full_content, 0, 2997) . '...'; } return $full_content; } private function synthesize_speech($text, $post_id) { if (!$this->polly_client) { error_log('ATPC: Polly client not initialized'); return false; } try { // 获取语音设置 $options = get_option('atpc_settings'); $voice_id = isset($options['voice_id']) ? $options['voice_id'] : 'Zhiyu'; $engine = isset($options['engine']) ? $options['engine'] : 'standard'; $language_code = isset($options['language_code']) ? $options['language_code'] : 'cmn-CN'; // 调用Polly API $result = $this->polly_client->synthesizeSpeech([ 'Text' => $text, 'OutputFormat' => 'mp3', 'VoiceId' => $voice_id, 'Engine' => $engine, 'LanguageCode' => $language_code, 'TextType' => 'text' ]); // 保存音频文件 $upload_dir = wp_upload_dir(); $audio_dir = $upload_dir['basedir'] . '/atpc-audio/'; if (!file_exists($audio_dir)) { wp_mkdir_p($audio_dir); } $filename = 'post-' . $post_id . '-' . time() . '.mp3'; $filepath = $audio_dir . $filename; // 保存音频数据 $audio_data = $result->get('AudioStream')->getContents(); file_put_contents($filepath, $audio_data); // 返回音频URL return $upload_dir['baseurl'] . '/atpc-audio/' . $filename; } catch (Exception $e) { error_log('ATPC: Failed to synthesize speech - ' . $e->getMessage()); return false; } } private function calculate_duration($text) { // 粗略估算:平均阅读速度约为150字/分钟 $word_count = str_word_count($text); $minutes = ceil($word_count / 150); // 格式化为HH:MM:SS $hours = floor($minutes / 60); $minutes = $minutes % 60; $seconds = 0; return sprintf('%02d:%02d:%02d', $hours, $minutes, $seconds); } public function get_available_voices() { if (!$this->polly_client) { return []; } try { $result = $this->polly_client->describeVoices(); $voices = $result->get('Voices'); $voice_list = []; foreach ($voices as $voice) { if (strpos($voice['LanguageCode'], 'zh') === 0 || strpos($voice['LanguageCode'], 'cmn') === 0) { $voice_list[] = [ 'id' => $voice['Id'], 'name' => $voice['Name'], 'language' => $voice['LanguageName'], 'gender' => $voice['Gender'] ]; } } return $voice_list; } catch (Exception $e) { error_log('ATPC: Failed to fetch voices - ' . $e->getMessage()); return []; } } } 第三部分:播客Feed生成与管理 3.1 理解播客RSS Feed规范 播客本质上是一个特殊的RSS Feed,包含一些额外的标签。关键的播客标签包括: <itunes:title>:播客标题 <itunes:author>:作者 <itunes:image>:播客封面 <itunes:category>:分类 <itunes:explicit>:是否包含成人内容 <itunes:duration>:音频时长 <enclosure>:音频文件URL、类型和大小 3.2 实现播客Feed类 创建includes/class-podcast-feed.php文件: <?php class ATPC_Podcast_Feed { private $feed_slug = 'podcast'; public function __construct() { // 添加播客Feed端点 add_action('init', [$this, 'add_podcast_feed_endpoint']); add_action('template_redirect', [$this, 'generate_podcast_feed']); // 添加播客头部信息 add_action('wp_head', [$this, 'add_podcast_feed_link']); } public function add_podcast_feed_endpoint() { add_rewrite_endpoint($this->feed_slug, EP_ROOT); add_rewrite_rule('^podcast/?$', 'index.php?podcast=feed', 'top'); add_rewrite_rule('^podcast/feed/?$', 'index.php?podcast=feed', 'top'); } public function generate_podcast_feed() { if (get_query_var('podcast') !== 'feed') { return; } // 设置内容类型为XML header('Content-Type: application/rss+xml; charset=' . get_option('blog_charset'), true); // 获取播客设置 $options = get_option('atpc_podcast_settings'); // 开始输出XML echo '<?xml version="1.0" encoding="' . get_option('blog_charset') . '"?>'; echo '<rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:content="http://purl.org/rss/1.0/modules/content/">'; echo '<channel>'; // 频道信息 echo '<title>' . esc_html($options['title'] ?? get_bloginfo('name') . '播客') . '</title>'; echo '<link>' . esc_url(home_url()) . '</link>'; echo '<language>' . get_bloginfo('language') . '</language>'; echo '<copyright>' . esc_html($options['copyright'] ?? '版权所有 ' . date('Y') . ' ' . get_bloginfo('name')) . '</copyright>'; echo '<itunes:author>' . esc_html($options['author'] ?? get_bloginfo('name')) . '</itunes:author>'; echo '<description>' . esc_html($options['description'] ?? get_bloginfo('description')) . '</description>'; // 播客封面 if (!empty($options['cover_image'])) { echo '<itunes:image href="' . esc_url($options['cover_image']) . '" />'; } // 分类 if (!empty($options['category'])) { echo '<itunes:category text="' . esc_attr($options['category']) . '" />'; } // 是否包含成人内容 echo '<itunes:explicit>' . ($options['explicit'] ?? 'no') . '</itunes:explicit>'; // 获取有音频的文章 $args = [ 'post_type' => get_option('atpc_post_types', ['post']), 'posts_per_page' => 50, 'meta_query' => [ [ 'key' => '_atpc_audio_url', 'compare' => 'EXISTS' ] ], 'orderby' => 'date', 'order' => 'DESC' ]; $podcast_posts = new WP_Query($args); 作为播客项目 if ($podcast_posts->have_posts()) { while ($podcast_posts->have_posts()) { $podcast_posts->the_post(); global $post; $audio_url = get_post_meta($post->ID, '_atpc_audio_url', true); $audio_duration = get_post_meta($post->ID, '_atpc_audio_duration', true); if (!$audio_url) { continue; } echo '<item>'; echo '<title>' . esc_html(get_the_title()) . '</title>'; echo '<link>' . esc_url(get_permalink()) . '</link>'; echo '<guid isPermaLink="false">' . esc_url($audio_url) . '</guid>'; echo '<pubDate>' . get_post_time('r', true) . '</pubDate>'; echo '<description><![CDATA[' . get_the_excerpt() . ']]></description>'; echo '<content:encoded><![CDATA[' . get_the_content() . ']]></content:encoded>'; // 作者信息 $author = get_the_author(); echo '<itunes:author>' . esc_html($author) . '</itunes:author>'; // 音频时长 if ($audio_duration) { echo '<itunes:duration>' . esc_html($audio_duration) . '</itunes:duration>'; } // 音频文件 $audio_size = $this->get_remote_file_size($audio_url); echo '<enclosure url="' . esc_url($audio_url) . '" length="' . esc_attr($audio_size) . '" type="audio/mpeg" />'; // 分类 $categories = get_the_category(); if (!empty($categories)) { echo '<category>' . esc_html($categories[0]->name) . '</category>'; } echo '</item>'; } wp_reset_postdata(); } echo '</channel>'; echo '</rss>'; exit; } private function get_remote_file_size($url) { // 尝试获取文件大小 $headers = get_headers($url, 1); if (isset($headers['Content-Length'])) { return $headers['Content-Length']; } // 如果无法获取,使用默认值 return '1048576'; // 1MB默认值 } public function add_podcast_feed_link() { $feed_url = home_url('/podcast/'); echo '<link rel="alternate" type="application/rss+xml" title="' . esc_attr(get_bloginfo('name') . '播客') . '" href="' . esc_url($feed_url) . '" />'; } public function get_feed_url() { return home_url('/podcast/'); } public function submit_to_podcast_directories() { $options = get_option('atpc_podcast_settings'); $feed_url = $this->get_feed_url(); $directories = [ 'itunes' => 'https://podcasts.apple.com/podcasts/submit', 'google' => 'https://podcastsmanager.google.com/', 'spotify' => 'https://podcasters.spotify.com/submit', 'amazon' => 'https://podcasters.amazon.com/', ]; $submission_links = []; foreach ($directories as $platform => $url) { $submission_links[$platform] = [ 'url' => $url, 'feed_param' => '?feed=' . urlencode($feed_url) ]; } return $submission_links; } } ## 第四部分:管理界面设计与实现 ### 4.1 创建插件设置页面 创建`includes/class-admin-ui.php`文件: <?phpclass ATPC_Admin_UI { public function __construct() { // 添加管理菜单 add_action('admin_menu', [$this, 'add_admin_menu']); // 注册设置 add_action('admin_init', [$this, 'register_settings']); // 添加文章列表音频列 add_filter('manage_posts_columns', [$this, 'add_audio_column']); add_action('manage_posts_custom_column', [$this, 'display_audio_column'], 10, 2); // 添加批量操作 add_filter('bulk_actions-edit-post', [$this, 'add_bulk_actions']); add_filter('handle_bulk_actions-edit-post', [$this, 'handle_bulk_actions'], 10, 3); // 添加文章编辑框元数据 add_action('add_meta_boxes', [$this, 'add_audio_meta_box']); add_action('save_post', [$this, 'save_audio_meta_box'], 10, 2); // 添加脚本和样式 add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']); } public static function activate_plugin() { // 创建必要的数据库表 global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'atpc_audio_logs'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id bigint(20) NOT NULL AUTO_INCREMENT, post_id bigint(20) NOT NULL, audio_url varchar(500) NOT NULL, generated_at datetime DEFAULT CURRENT_TIMESTAMP, status varchar(20) DEFAULT 'success', error_message text, PRIMARY KEY (id), KEY post_id (post_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 设置默认选项 $default_settings = [ 'aws_access_key' => '', 'aws_secret_key' => '', 'aws_region' => 'us-east-1', 'voice_id' => 'Zhiyu', 'engine' => 'standard', 'language_code' => 'cmn-CN' ]; add_option('atpc_settings', $default_settings); $default_podcast_settings = [ 'title' => get_bloginfo('name') . '播客', 'author' => get_bloginfo('name'), 'description' => get_bloginfo('description'), 'cover_image' => '', 'category' => 'Technology', 'explicit' => 'no', 'copyright' => '版权所有 ' . date('Y') . ' ' . get_bloginfo('name') ]; add_option('atpc_podcast_settings', $default_podcast_settings); add_option('atpc_auto_generate', 'yes'); add_option('atpc_post_types', ['post']); // 刷新重写规则 flush_rewrite_rules(); } public static function deactivate_plugin() { // 清理临时数据 // 注意:不删除设置和音频文件,以便重新激活时继续使用 flush_rewrite_rules(); } public function add_admin_menu() { // 主菜单 add_menu_page( __('文章转播客', 'article-to-podcast'), __('文章转播客', 'article-to-podcast'), 'manage_options', 'article-to-podcast', [$this, 'display_main_page'], 'dashicons-controls-volumeon', 30 ); // 子菜单 add_submenu_page( 'article-to-podcast', __('设置', 'article-to-podcast'), __('设置', 'article-to-podcast'), 'manage_options', 'atpc-settings', [$this, 'display_settings_page'] ); add_submenu_page( 'article-to-podcast', __('播客设置', 'article-to-podcast'), __('播客设置', 'article-to-podcast'), 'manage_options', 'atpc-podcast-settings', [$this, 'display_podcast_settings_page'] ); add_submenu_page( 'article-to-podcast', __('批量生成', 'article-to-podcast'), __('批量生成', 'article-to-podcast'), 'manage_options', 'atpc-batch-generate', [$this, 'display_batch_generate_page'] ); add_submenu_page( 'article-to-podcast', __('统计', 'article-to-podcast'), __('统计', 'article-to-podcast'), 'manage_options', 'atpc-stats', [$this, 'display_stats_page'] ); } public function display_main_page() { ?> <div class="wrap atpc-dashboard"> <h1><?php echo esc_html(get_admin_page_title()); ?></h1> <div class="atpc-stats-cards"> <div class="card"> <h3><?php _e('已生成音频', 'article-to-podcast'); ?></h3> <p class="number"><?php echo $this->get_audio_count(); ?></p> </div> <div class="card"> <h3><?php _e('播客订阅', 'article-to-podcast'); ?></h3> <p class="number"><?php echo $this->get_feed_url(); ?></p> </div> <div class="card"> <h3><?php _e('最近生成', 'article-to-podcast'); ?></h3> <p class="number"><?php echo $this->get_recent_activity(); ?></p> </div> </div> <div class="atpc-quick-actions"> <h2><?php _e('快速操作', 'article-to-podcast'); ?></h2> <div class="action-buttons"> <a href="<?php echo admin_url('admin.php?page=atpc-batch-generate'); ?>" class="button button-primary"> <?php _e('批量生成音频', 'article-to-podcast'); ?> </a> <a href="<?php echo home_url('/podcast/'); ?>" target="_blank" class="button"> <?php _e('查看播客Feed', 'article-to-podcast'); ?> </a> <a href="<?php echo admin_url('admin.php?page=atpc-podcast-settings'); ?>" class="button"> <?php _e('播客目录提交', 'article-to-podcast'); ?> </a> </div> </div> <div class="atpc-recent-audio"> <h2><?php _e('最近生成的音频', 'article-to-podcast'); ?></h2> <?php $this->display_recent_audio_table(); ?> </div> </div> <?php } public function display_settings_page() { ?> <div class="wrap"> <h1><?php _e('TTS服务设置', 'article-to-podcast'); ?></h1> <form method="post" action="options.php"> <?php settings_fields('atpc_settings_group'); do_settings_sections('atpc-settings'); submit_button(); ?> </form> <div class="atpc-test-section"> <h2><?php _e('测试TTS服务', 'article-to-podcast'); ?></h2> <textarea id="atpc-test-text" rows="4" style="width: 100%;" placeholder="<?php esc_attr_e('输入要测试的文字...', 'article-to-podcast'); ?>"></textarea> <button id="atpc-test-tts" class="button button-secondary"> <?php _e('测试语音合成', 'article-to-podcast'); ?> </button> <div id="atpc-test-result"></div> </div> </div> <?php } public function register_settings() { // TTS设置 register_setting('atpc_settings_group', 'atpc_settings'); register_setting('atpc_settings_group', 'atpc_auto_generate'); register_setting('atpc_settings_group', 'atpc_post_types'); // 播客设置 register_setting('atpc_podcast_group', 'atpc_podcast_settings'); // TTS设置部分 add_settings_section( 'atpc_tts_section', __('文字转语音服务设置', 'article-to-podcast'), [$this, 'tts_section_callback'], 'atpc-settings' ); // AWS凭证字段 add_settings_field( 'aws_access_key', __('AWS访问密钥', 'article-to-podcast'), [$this, 'text_field_callback'], 'atpc-settings', 'atpc_tts_section', [ 'label_for' => 'aws_access_key', 'option_group' => 'atpc_settings', 'description' => __('Amazon Polly服务的Access Key ID', 'article-to-podcast') ] ); // 更多设置字段... } public function text_field_callback($args) { $option_group = $args['option_group']; $field_name = $args['label_for']; $options = get_option($option_group); $value = isset($options[$field_name]) ? $options[$field_name] : ''; echo '<input type="text" id="' . esc_attr($field_name) . '" name="' . esc_attr($option_group) . '[' . esc_attr($field_name) . ']" value="' . esc_attr($value) . '" class="regular-text">'; if (!empty($args['description'])) { echo '<p class="description">' . esc_html($args['description']) . '</p>'; } } public function add_audio_column($columns) { $columns['atpc_audio'] = __('音频', 'article-to-podcast'); return $columns; } public function display_audio_column($column, $post_id) { if ($column === 'atpc_audio') { $audio_url = get_post_meta($post_id, '_atpc_audio_url', true); if ($audio_url) { echo '<a href="' . esc_url($audio_url) . '" target="_blank" class="button button-small">'; echo __('播放', 'article-to-podcast'); echo '</a>'; echo '<button class="button button-small atpc-regenerate" data-post-id="' . esc_attr($post_id) . '">'; echo __('重新生成', 'article-to-podcast'); echo '</button>'; } else { echo '<button class="button button-small button-primary atpc-generate" data-post-id="' . esc_attr($post_id) . '">'; echo __('生成音频', 'article-to-podcast'); echo '</button>'; } } } public function add_bulk_actions($bulk_actions) { $bulk_actions['generate_audio'] = __('生成音频', 'article-to-podcast'); $bulk_actions['regenerate_audio'] = __('重新生成音频', 'article-to-podcast'); return $bulk_actions; } public function handle_bulk_actions($redirect_to, $doaction, $post_ids) { if ($doaction === 'generate_audio' || $doaction === 'regenerate_audio') { $tts_engine = new ATPC_TTS_Engine(); $processed = 0; foreach ($post_ids as $post_id) { if ($tts_engine->generate_audio($post_id)) { $processed++; } } $redirect_to = add_query_arg('bulk_audio_processed', $processed, $redirect_to); } return $redirect_to; } public function add_audio_meta_box() { $post_types = get_option('atpc_post_types', ['post']); foreach ($post_types as $post_type) { add_meta_box( 'atpc_audio_meta_box', __('文章音频', 'article-to-podcast'), [$this, 'render_audio_meta_box'], $post_type, 'side', 'high' ); } } public function render_audio_meta_box($post) { wp_nonce_field('atpc_audio_meta_box', 'atpc_audio_meta_box_nonce'); $audio_url = get_post_meta($post->ID, '_atpc_audio_url', true); $generated_time = get_post_meta($post->ID, '_atpc_audio_generated', true); if ($audio_url) { echo '<audio controls style="width: 100%; margin-bottom: 10px;">'; echo '<source src="' . esc_url($audio_url) . '" type="audio/mpeg">'; echo __('您的浏览器不支持音频播放。', 'article-to-podcast'); echo '</audio>'; echo '<p><strong>' . __('音频URL:', 'article-to-podcast') . '</strong><br>'; echo '<input type="text" readonly value="' . esc_url($audio_url) . '" style="width: 100%; font-size: 11px;"></p>'; if ($generated_time) { echo '<p><strong>' . __('生成时间:', 'article-to-podcast') . '</strong><br>'; echo esc_html($generated_time) . '</p>'; } echo '<button type="button" class="button button-secondary atpc-regenerate-single" data-post-id="' . esc_attr($post->ID) . '">'; echo __('重新生成音频', 'article-to-podcast'); echo '</button>'; echo '<button type="button" class="button atpc-copy-url" data-url="' . esc_url($audio_url) . '">'; echo __('复制URL', 'article-to-podcast'); echo '</button>'; } else { echo '<p>' . __('此文章
发表评论实战教程:在WordPress网站中添加在线个人记账与预算规划管理小程序 摘要 本教程将详细介绍如何通过WordPress代码二次开发,在您的网站上添加一个功能完整的在线个人记账与预算规划管理小程序。我们将从零开始,逐步构建一个包含收入支出记录、预算管理、数据可视化等核心功能的实用工具,帮助您的网站访客更好地管理个人财务。 目录 项目概述与准备工作 WordPress开发环境搭建 数据库设计与表结构创建 用户界面设计与前端开发 后端API与数据处理逻辑 预算规划功能实现 数据可视化与报表生成 安全性与数据保护 性能优化与部署 功能扩展与维护建议 1. 项目概述与准备工作 1.1 项目目标 本项目的目标是在WordPress网站中集成一个完整的个人财务管理工具,使访客能够: 记录日常收入和支出 设置和管理预算 查看财务数据可视化报表 获得支出分类分析 导出财务数据 1.2 技术栈选择 前端:HTML5, CSS3, JavaScript (使用jQuery简化开发) 后端:PHP (WordPress原生支持) 数据库:MySQL (通过WordPress数据库操作类) 图表库:Chart.js (轻量级、响应式图表) UI框架:Bootstrap 5 (快速构建响应式界面) 1.3 准备工作 确保您拥有一个已安装的WordPress网站 具备基本的PHP、JavaScript和MySQL知识 准备一个代码编辑器(如VS Code、Sublime Text等) 备份您的WordPress网站,以防开发过程中出现问题 2. WordPress开发环境搭建 2.1 创建插件目录结构 在WordPress的wp-content/plugins/目录下创建一个新文件夹personal-finance-manager,并建立以下结构: personal-finance-manager/ ├── personal-finance-manager.php # 主插件文件 ├── includes/ │ ├── class-database.php # 数据库操作类 │ ├── class-shortcodes.php # 短代码处理类 │ ├── class-api.php # API处理类 │ └── class-charts.php # 图表生成类 ├── assets/ │ ├── css/ │ │ └── style.css # 样式文件 │ ├── js/ │ │ └── script.js # 前端脚本 │ └── lib/ │ └── chart.min.js # Chart.js库 ├── templates/ │ ├── dashboard.php # 主仪表板模板 │ ├── add-transaction.php # 添加交易模板 │ └── budget.php # 预算管理模板 └── languages/ # 国际化文件目录 2.2 创建主插件文件 在personal-finance-manager.php中添加以下代码: <?php /** * Plugin Name: 个人记账与预算规划管理 * Plugin URI: https://yourwebsite.com/ * Description: 在WordPress网站中添加在线个人记账与预算规划管理功能 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later * Text Domain: personal-finance-manager */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('PFM_VERSION', '1.0.0'); define('PFM_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('PFM_PLUGIN_URL', plugin_dir_url(__FILE__)); // 加载必要文件 require_once PFM_PLUGIN_DIR . 'includes/class-database.php'; require_once PFM_PLUGIN_DIR . 'includes/class-shortcodes.php'; require_once PFM_PLUGIN_DIR . 'includes/class-api.php'; require_once PFM_PLUGIN_DIR . 'includes/class-charts.php'; // 初始化插件 class Personal_Finance_Manager { private static $instance = null; public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } private function __construct() { $this->init_hooks(); } private function init_hooks() { // 激活/停用钩子 register_activation_hook(__FILE__, array($this, 'activate')); register_deactivation_hook(__FILE__, array($this, 'deactivate')); // 初始化 add_action('init', array($this, 'init')); // 加载脚本和样式 add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts')); // 初始化短代码 $shortcodes = new PFM_Shortcodes(); $shortcodes->init(); } public function activate() { // 创建数据库表 PFM_Database::create_tables(); // 设置默认选项 update_option('pfm_version', PFM_VERSION); } public function deactivate() { // 清理临时数据 // 注意:这里不删除用户数据,仅清理临时选项 delete_option('pfm_temp_data'); } public function init() { // 加载文本域 load_plugin_textdomain('personal-finance-manager', false, dirname(plugin_basename(__FILE__)) . '/languages'); } public function enqueue_scripts() { // 仅在有需要的页面加载 if (is_page('personal-finance') || has_shortcode(get_post()->post_content, 'personal_finance')) { // 加载CSS wp_enqueue_style('pfm-bootstrap', 'https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css'); wp_enqueue_style('pfm-style', PFM_PLUGIN_URL . 'assets/css/style.css', array(), PFM_VERSION); // 加载JavaScript wp_enqueue_script('jquery'); wp_enqueue_script('pfm-bootstrap', 'https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js', array('jquery'), '5.1.3', true); wp_enqueue_script('pfm-chartjs', PFM_PLUGIN_URL . 'assets/lib/chart.min.js', array(), '3.7.0', true); wp_enqueue_script('pfm-script', PFM_PLUGIN_URL . 'assets/js/script.js', array('jquery', 'pfm-chartjs'), PFM_VERSION, true); // 传递数据到前端 wp_localize_script('pfm-script', 'pfm_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('pfm_nonce'), 'user_id' => get_current_user_id() )); } } } // 启动插件 Personal_Finance_Manager::get_instance(); 3. 数据库设计与表结构创建 3.1 数据库表设计 我们需要创建三个主要表来存储财务数据: 交易记录表:存储收入和支出记录 预算表:存储用户设置的预算 分类表:存储收入和支出的分类 3.2 数据库操作类实现 在includes/class-database.php中添加以下代码: <?php class PFM_Database { private static $table_prefix; public static function init() { global $wpdb; self::$table_prefix = $wpdb->prefix . 'pfm_'; } public static function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); self::$table_prefix = $wpdb->prefix . 'pfm_'; // 交易记录表 $transactions_table = self::$table_prefix . 'transactions'; $sql_transactions = "CREATE TABLE IF NOT EXISTS $transactions_table ( id INT(11) NOT NULL AUTO_INCREMENT, user_id INT(11) NOT NULL, type ENUM('income', 'expense') NOT NULL, category_id INT(11) NOT NULL, amount DECIMAL(10,2) NOT NULL, description TEXT, transaction_date DATE NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), INDEX user_id (user_id), INDEX transaction_date (transaction_date) ) $charset_collate;"; // 预算表 $budgets_table = self::$table_prefix . 'budgets'; $sql_budgets = "CREATE TABLE IF NOT EXISTS $budgets_table ( id INT(11) NOT NULL AUTO_INCREMENT, user_id INT(11) NOT NULL, category_id INT(11) NOT NULL, amount DECIMAL(10,2) NOT NULL, period ENUM('daily', 'weekly', 'monthly', 'yearly') DEFAULT 'monthly', start_date DATE NOT NULL, end_date DATE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), INDEX user_id (user_id) ) $charset_collate;"; // 分类表 $categories_table = self::$table_prefix . 'categories'; $sql_categories = "CREATE TABLE IF NOT EXISTS $categories_table ( id INT(11) NOT NULL AUTO_INCREMENT, user_id INT(11) DEFAULT 0, name VARCHAR(100) NOT NULL, type ENUM('income', 'expense') NOT NULL, color VARCHAR(7) DEFAULT '#007bff', icon VARCHAR(50) DEFAULT 'fas fa-tag', is_default TINYINT(1) DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), INDEX user_id (user_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql_transactions); dbDelta($sql_budgets); dbDelta($sql_categories); // 插入默认分类 self::insert_default_categories(); } private static function insert_default_categories() { global $wpdb; $categories_table = self::$table_prefix . 'categories'; $default_categories = array( // 收入分类 array('name' => '工资', 'type' => 'income', 'color' => '#28a745', 'icon' => 'fas fa-money-check-alt', 'is_default' => 1), array('name' => '投资', 'type' => 'income', 'color' => '#20c997', 'icon' => 'fas fa-chart-line', 'is_default' => 1), array('name' => '兼职', 'type' => 'income', 'color' => '#17a2b8', 'icon' => 'fas fa-briefcase', 'is_default' => 1), array('name' => '其他收入', 'type' => 'income', 'color' => '#6c757d', 'icon' => 'fas fa-coins', 'is_default' => 1), // 支出分类 array('name' => '餐饮', 'type' => 'expense', 'color' => '#dc3545', 'icon' => 'fas fa-utensils', 'is_default' => 1), array('name' => '交通', 'type' => 'expense', 'color' => '#fd7e14', 'icon' => 'fas fa-car', 'is_default' => 1), array('name' => '购物', 'type' => 'expense', 'color' => '#6f42c1', 'icon' => 'fas fa-shopping-cart', 'is_default' => 1), array('name' => '住房', 'type' => 'expense', 'color' => '#e83e8c', 'icon' => 'fas fa-home', 'is_default' => 1), array('name' => '娱乐', 'type' => 'expense', 'color' => '#20c997', 'icon' => 'fas fa-gamepad', 'is_default' => 1), array('name' => '医疗', 'type' => 'expense', 'color' => '#007bff', 'icon' => 'fas fa-heartbeat', 'is_default' => 1), array('name' => '教育', 'type' => 'expense', 'color' => '#17a2b8', 'icon' => 'fas fa-graduation-cap', 'is_default' => 1), array('name' => '其他支出', 'type' => 'expense', 'color' => '#6c757d', 'icon' => 'fas fa-tags', 'is_default' => 1), ); foreach ($default_categories as $category) { $wpdb->insert( $categories_table, $category, array('%s', '%s', '%s', '%s', '%d') ); } } // 获取用户交易记录 public static function get_transactions($user_id, $limit = 50, $offset = 0, $filters = array()) { global $wpdb; $transactions_table = self::$table_prefix . 'transactions'; $categories_table = self::$table_prefix . 'categories'; $where_clause = "WHERE t.user_id = %d"; $where_values = array($user_id); // 应用过滤器 if (!empty($filters['type'])) { $where_clause .= " AND t.type = %s"; $where_values[] = $filters['type']; } if (!empty($filters['category_id'])) { $where_clause .= " AND t.category_id = %d"; $where_values[] = $filters['category_id']; } if (!empty($filters['start_date'])) { $where_clause .= " AND t.transaction_date >= %s"; $where_values[] = $filters['start_date']; } if (!empty($filters['end_date'])) { $where_clause .= " AND t.transaction_date <= %s"; $where_values[] = $filters['end_date']; } $query = $wpdb->prepare( "SELECT t.*, c.name as category_name, c.color as category_color, c.icon as category_icon FROM $transactions_table t LEFT JOIN $categories_table c ON t.category_id = c.id $where_clause ORDER BY t.transaction_date DESC, t.created_at DESC LIMIT %d OFFSET %d", array_merge($where_values, array($limit, $offset)) ); return $wpdb->get_results($query); } // 添加交易记录 public static function add_transaction($data) { global $wpdb; $transactions_table = self::$table_prefix . 'transactions'; $result = $wpdb->insert( $transactions_table, $data, array('%d', '%s', '%d', '%f', '%s', '%s') ); return $result ? $wpdb->insert_id : false; } // 获取用户预算 public static function get_budgets($user_id, $period = 'monthly', $date = null) { global $wpdb; if (!$date) { $date = date('Y-m-d'); } $budgets_table = self::$table_prefix . 'budgets'; $categories_table = self::$table_prefix . 'categories'; $query = $wpdb->prepare( "SELECT b.*, c.name as category_name, c.type as category_type FROM $budgets_table b LEFT JOIN $categories_table c ON b.category_id = c.id WHERE b.user_id = %d AND b.period = %s AND (b.end_date IS NULL OR b.end_date >= %s) ORDER BY b.start_date DESC", array($user_id, $period, $date) ); return $wpdb->get_results($query); } // 更多数据库操作方法... } PFM_Database::init(); 4. 用户界面设计与前端开发 4.1 创建短代码类 在includes/class-shortcodes.php中添加以下代码: <?php class PFM_Shortcodes { public function init() { add_shortcode('personal_finance', array($this, 'render_main_dashboard')); add_shortcode('add_transaction', array($this, 'render_add_transaction')); add_shortcode('budget_planner', array($this, 'render_budget_planner')); } public function render_main_dashboard($atts) { // 检查用户是否登录 if (!is_user_logged_in()) { return $this->render_login_prompt(); } // 获取模板 ob_start(); include PFM_PLUGIN_DIR . 'templates/dashboard.php'; return ob_get_clean(); } public function render_add_transaction($atts) { if (!is_user_logged_in()) { return $this->render_login_prompt(); } ob_start(); include PFM_PLUGIN_DIR . 'templates/add-transaction.php'; return ob_get_clean(); } public function render_budget_planner($atts) { if (!is_user_logged_in()) { return $this->render_login_prompt(); } ob_start(); include PFM_PLUGIN_DIR . 'templates/budget.php'; return ob_get_clean(); } private function render_login_prompt() { get_permalink() . '">登录以使用个人记账功能。</div>'; } } ### 4.2 主仪表板模板 在`templates/dashboard.php`中添加以下代码: <div class="personal-finance-dashboard"> <div class="container-fluid"> <div class="row mb-4"> <div class="col-12"> <h1 class="h3 mb-0">个人财务管理中心</h1> <p class="text-muted">管理您的收入、支出和预算</p> </div> </div> <!-- 快速统计卡片 --> <div class="row mb-4"> <div class="col-md-3 mb-3"> <div class="card border-left-primary shadow h-100 py-2"> <div class="card-body"> <div class="row no-gutters align-items-center"> <div class="col mr-2"> <div class="text-xs font-weight-bold text-primary text-uppercase mb-1"> 本月收入</div> <div class="h5 mb-0 font-weight-bold text-gray-800" id="current-month-income">¥0.00</div> </div> <div class="col-auto"> <i class="fas fa-money-bill-wave fa-2x text-gray-300"></i> </div> </div> </div> </div> </div> <div class="col-md-3 mb-3"> <div class="card border-left-danger shadow h-100 py-2"> <div class="card-body"> <div class="row no-gutters align-items-center"> <div class="col mr-2"> <div class="text-xs font-weight-bold text-danger text-uppercase mb-1"> 本月支出</div> <div class="h5 mb-0 font-weight-bold text-gray-800" id="current-month-expense">¥0.00</div> </div> <div class="col-auto"> <i class="fas fa-shopping-cart fa-2x text-gray-300"></i> </div> </div> </div> </div> </div> <div class="col-md-3 mb-3"> <div class="card border-left-success shadow h-100 py-2"> <div class="card-body"> <div class="row no-gutters align-items-center"> <div class="col mr-2"> <div class="text-xs font-weight-bold text-success text-uppercase mb-1"> 本月结余</div> <div class="h5 mb-0 font-weight-bold text-gray-800" id="current-month-balance">¥0.00</div> </div> <div class="col-auto"> <i class="fas fa-piggy-bank fa-2x text-gray-300"></i> </div> </div> </div> </div> </div> <div class="col-md-3 mb-3"> <div class="card border-left-warning shadow h-100 py-2"> <div class="card-body"> <div class="row no-gutters align-items-center"> <div class="col mr-2"> <div class="text-xs font-weight-bold text-warning text-uppercase mb-1"> 预算使用率</div> <div class="h5 mb-0 font-weight-bold text-gray-800" id="budget-usage">0%</div> </div> <div class="col-auto"> <i class="fas fa-chart-pie fa-2x text-gray-300"></i> </div> </div> </div> </div> </div> </div> <!-- 主要内容和图表 --> <div class="row"> <!-- 左侧:图表区域 --> <div class="col-lg-8 mb-4"> <div class="card shadow mb-4"> <div class="card-header py-3 d-flex flex-row align-items-center justify-content-between"> <h6 class="m-0 font-weight-bold text-primary">月度收支趋势</h6> <div class="dropdown no-arrow"> <select class="form-control form-control-sm" id="chart-period"> <option value="3">最近3个月</option> <option value="6" selected>最近6个月</option> <option value="12">最近12个月</option> </select> </div> </div> <div class="card-body"> <div class="chart-area"> <canvas id="monthlyTrendChart"></canvas> </div> </div> </div> <div class="row"> <div class="col-lg-6 mb-4"> <div class="card shadow"> <div class="card-header py-3"> <h6 class="m-0 font-weight-bold text-primary">支出分类</h6> </div> <div class="card-body"> <div class="chart-pie pt-4"> <canvas id="expenseCategoryChart"></canvas> </div> </div> </div> </div> <div class="col-lg-6 mb-4"> <div class="card shadow"> <div class="card-header py-3"> <h6 class="m-0 font-weight-bold text-primary">预算执行情况</h6> </div> <div class="card-body"> <div id="budget-progress-container"> <!-- 预算进度条将通过JS动态生成 --> <div class="text-center py-4"> <div class="spinner-border text-primary" role="status"> <span class="visually-hidden">加载中...</span> </div> </div> </div> </div> </div> </div> </div> </div> <!-- 右侧:快速操作和最近交易 --> <div class="col-lg-4 mb-4"> <!-- 快速操作 --> <div class="card shadow mb-4"> <div class="card-header py-3"> <h6 class="m-0 font-weight-bold text-primary">快速操作</h6> </div> <div class="card-body"> <div class="d-grid gap-2"> <button class="btn btn-primary" id="btn-add-income"> <i class="fas fa-plus-circle me-2"></i>添加收入 </button> <button class="btn btn-danger" id="btn-add-expense"> <i class="fas fa-minus-circle me-2"></i>添加支出 </button> <button class="btn btn-success" id="btn-manage-budget"> <i class="fas fa-chart-line me-2"></i>管理预算 </button> <button class="btn btn-info" id="btn-export-data"> <i class="fas fa-download me-2"></i>导出数据 </button> </div> </div> </div> <!-- 最近交易 --> <div class="card shadow"> <div class="card-header py-3 d-flex flex-row align-items-center justify-content-between"> <h6 class="m-0 font-weight-bold text-primary">最近交易</h6> <a href="#" class="btn btn-sm btn-outline-primary" id="btn-view-all">查看全部</a> </div> <div class="card-body"> <div id="recent-transactions"> <!-- 最近交易列表将通过JS动态加载 --> <div class="text-center py-4"> <div class="spinner-border text-primary" role="status"> <span class="visually-hidden">加载中...</span> </div> </div> </div> </div> </div> </div> </div> </div> </div> <!-- 添加交易模态框 --><div class="modal fade" id="addTransactionModal" tabindex="-1" aria-hidden="true"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" id="transactionModalTitle">添加交易</h5> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> <form id="transaction-form"> <div class="mb-3"> <label for="transaction-type" class="form-label">交易类型</label> <div class="btn-group w-100" role="group"> <input type="radio" class="btn-check" name="transaction-type" id="type-income" value="income" autocomplete="off" checked> <label class="btn btn-outline-success" for="type-income">收入</label> <input type="radio" class="btn-check" name="transaction-type" id="type-expense" value="expense" autocomplete="off"> <label class="btn btn-outline-danger" for="type-expense">支出</label> </div> </div> <div class="mb-3"> <label for="transaction-amount" class="form-label">金额</label> <div class="input-group"> <span class="input-group-text">¥</span> <input type="number" class="form-control" id="transaction-amount" step="0.01" min="0.01" required> </div> </div> <div class="mb-3"> <label for="transaction-category" class="form-label">分类</label> <select class="form-select" id="transaction-category" required> <!-- 分类选项将通过JS动态加载 --> <option value="">请选择分类</option> </select> </div> <div class="mb-3"> <label for="transaction-date" class="form-label">日期</label> <input type="date" class="form-control" id="transaction-date" value="<?php echo date('Y-m-d'); ?>" required> </div> <div class="mb-3"> <label for="transaction-description" class="form-label">描述</label> <textarea class="form-control" id="transaction-description" rows="2" placeholder="请输入交易描述(可选)"></textarea> </div> <div class="d-grid"> <button type="submit" class="btn btn-primary" id="btn-save-transaction">保存交易</button> </div> </form> </div> </div> </div> </div> --- ## 5. 后端API与数据处理逻辑 ### 5.1 API处理类 在`includes/class-api.php`中添加以下代码: <?phpclass PFM_API { public function init() { // 注册AJAX处理函数 add_action('wp_ajax_pfm_add_transaction', array($this, 'add_transaction')); add_action('wp_ajax_nopriv_pfm_add_transaction', array($this, 'require_login')); add_action('wp_ajax_pfm_get_transactions', array($this, 'get_transactions')); add_action('wp_ajax_nopriv_pfm_get_transactions', array($this, 'require_login')); add_action('wp_ajax_pfm_get_statistics', array($this, 'get_statistics')); add_action('wp_ajax_nopriv_pfm_get_statistics', array($this, 'require_login')); add_action('wp_ajax_pfm_get_categories', array($this, 'get_categories')); add_action('wp_ajax_nopriv_pfm_get_categories', array($this, 'require_login')); add_action('wp_ajax_pfm_add_budget', array($this, 'add_budget')); add_action('wp_ajax_nopriv_pfm_add_budget', array($this, 'require_login')); add_action('wp_ajax_pfm_get_budgets', array($this, 'get_budgets')); add_action('wp_ajax_nopriv_pfm_get_budgets', array($this, 'require_login')); add_action('wp_ajax_pfm_export_data', array($this, 'export_data')); add_action('wp_ajax_nopriv_pfm_export_data', array($this, 'require_login')); } private function verify_nonce() { if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'pfm_nonce')) { wp_send_json_error(array('message' => '安全验证失败')); } } public function require_login() { wp_send_json_error(array('message' => '请先登录')); } // 添加交易记录 public function add_transaction() { $this->verify_nonce(); $user_id = get_current_user_id(); if (!$user_id) { wp_send_json_error(array('message' => '用户未登录')); } // 验证数据 $type = sanitize_text_field($_POST['type']); $category_id = intval($_POST['category_id']); $amount = floatval($_POST['amount']); $description = sanitize_textarea_field($_POST['description']); $transaction_date = sanitize_text_field($_POST['transaction_date']); if (!in_array($type, array('income', 'expense'))) { wp_send_json_error(array('message' => '无效的交易类型')); } if ($amount <= 0) { wp_send_json_error(array('message' => '金额必须大于0')); } if (empty($transaction_date)) { $transaction_date = date('Y-m-d'); } // 保存到数据库 $data = array( 'user_id' => $user_id, 'type' => $type, 'category_id' => $category_id, 'amount' => $amount, 'description' => $description, 'transaction_date' => $transaction_date ); $transaction_id = PFM_Database::add_transaction($data); if ($transaction_id) { wp_send_json_success(array( 'message' => '交易记录添加成功', 'transaction_id' => $transaction_id )); } else { wp_send_json_error(array('message' => '添加交易记录失败')); } } // 获取交易记录 public function get_transactions() { $this->verify_nonce(); $user_id = get_current_user_id(); if (!$user_id) { wp_send_json_error(array('message' => '用户未登录')); } $limit = isset($_POST['limit']) ? intval($_POST['limit']) : 50; $offset = isset($_POST['offset']) ? intval($_POST['offset']) : 0; $filters = array(); if (isset($_POST['type'])) { $filters['type'] = sanitize_text_field($_POST['type']); } if (isset($_POST['category_id'])) { $filters['category_id'] = intval($_POST['category_id']); } if (isset($_POST['start_date'])) { $filters['start_date'] = sanitize_text_field($_POST['start_date']); } if (isset($_POST['end_date'])) { $filters['end_date'] = sanitize_text_field($_POST['end_date']); } $transactions = PFM_Database::get_transactions($user_id, $limit, $offset, $filters); // 格式化数据 $formatted_transactions = array(); foreach ($transactions as $transaction) { $formatted_transactions[] = array( 'id' => $transaction->id, 'type' => $transaction->type, 'type_text' => $transaction->type == 'income' ? '收入' : '支出', 'amount' => number_format($transaction->amount, 2), 'amount_raw' => $transaction->amount, 'category_name' => $transaction->category_name, 'category_color' => $transaction->category_color, 'description' => $transaction->description, 'date' => date('Y-m-d', strtotime($transaction->transaction_date)), 'date_formatted' => date('m月d日', strtotime($transaction->transaction_date)), 'created_at' => $transaction->created_at ); } wp_send_json_success(array( 'transactions' => $formatted_transactions, 'count' => count($formatted_transactions) )); } // 获取统计数据 public function get_statistics() { $this->verify_nonce(); $user_id = get_current_user_id(); if (!$user_id) { wp_send_json_error(array('message' => '用户未登录')); } global $wpdb; $table_prefix = $wpdb->prefix . 'pfm_'; $transactions_table = $table_prefix . 'transactions'; // 获取当前月份 $current_month = date('Y-m'); // 本月收入 $monthly_income = $wpdb->get_var($wpdb->prepare( "SELECT SUM(amount) FROM $transactions_table WHERE user_id = %d AND type = 'income' AND DATE_FORMAT(transaction_date, '%%Y-%%m') = %s", $user_id, $current_month )); // 本月支出 $monthly_expense = $wpdb->get_var($wpdb->prepare( "SELECT SUM(amount) FROM $transactions_table WHERE user_id = %d AND type = 'expense' AND DATE_FORMAT(transaction_date, '%%Y-%%m') = %s", $user_id, $current_month )); // 月度趋势数据(最近6个月) $months = array(); $income_trend = array(); $expense_trend = array();
发表评论手把手教学:为WordPress集成智能化的网站表单数据验证与清洗工具 引言:为什么WordPress网站需要智能表单验证与清洗 在当今互联网环境中,网站表单是与用户互动的重要桥梁。无论是联系表单、注册表单、订单表单还是调查问卷,表单数据的质量直接影响到用户体验、数据分析和业务决策。然而,许多WordPress网站管理员面临一个共同问题:如何确保用户提交的表单数据既准确又安全? 传统的表单验证方法往往只停留在基础层面,如检查必填字段或简单的格式验证。但随着网络攻击手段的日益复杂和用户期望的提高,我们需要更智能的解决方案。本文将手把手教您如何通过WordPress代码二次开发,集成智能化的表单数据验证与清洗工具,将常用互联网小工具功能融入您的网站。 第一章:理解WordPress表单生态系统 1.1 WordPress常用表单插件分析 WordPress生态系统中有众多表单插件,如Contact Form 7、Gravity Forms、WPForms和Ninja Forms等。这些插件提供了丰富的表单构建功能,但在数据验证和清洗方面往往存在局限性: 基础验证功能:大多数插件提供必填字段、电子邮件格式、数字范围等基础验证 有限的清洗能力:对用户输入的潜在危险内容过滤不足 缺乏智能验证:无法识别虚假信息、垃圾内容或恶意输入 扩展性有限:自定义验证规则需要复杂的配置或额外插件 1.2 表单数据面临的主要风险 在集成智能验证工具前,我们需要了解表单数据面临的主要风险: 安全威胁:SQL注入、跨站脚本攻击(XSS)、跨站请求伪造(CSRF) 数据质量问题:虚假信息、格式错误、不一致的数据 垃圾信息:广告内容、垃圾邮件、机器人提交 用户体验问题:复杂的验证流程、不清晰的错误提示 1.3 智能验证与清洗的核心概念 智能表单验证与清洗不仅仅是检查数据格式,还包括: 上下文感知验证:根据字段类型和业务逻辑进行验证 实时反馈:在用户输入时提供即时验证反馈 数据标准化:将不同格式的数据转换为统一格式 威胁检测:识别并阻止恶意内容提交 垃圾信息过滤:使用多种技术识别和过滤垃圾提交 第二章:搭建开发环境与准备工作 2.1 开发环境配置 在开始二次开发前,确保您已准备好以下环境: 本地开发环境:推荐使用Local by Flywheel、XAMPP或MAMP 代码编辑器:VS Code、PHPStorm或Sublime Text WordPress安装:最新版本的WordPress 调试工具:安装Query Monitor、Debug Bar等调试插件 版本控制:Git用于代码版本管理 2.2 创建自定义插件 为了避免主题更新导致代码丢失,我们将创建一个独立插件来实现表单验证功能: 在wp-content/plugins/目录下创建新文件夹smart-form-validation 创建主插件文件smart-form-validation.php,添加插件头部信息: <?php /** * Plugin Name: Smart Form Validation & Sanitization * Plugin URI: https://yourwebsite.com/ * Description: 智能表单验证与清洗工具,增强WordPress表单安全性 * Version: 1.0.0 * Author: Your Name * License: GPL v2 or later * Text Domain: smart-form-validation */ 2.3 安全注意事项 在进行二次开发时,请牢记以下安全准则: 始终对用户输入进行验证、清洗和转义 使用WordPress非ce和权限检查功能 避免直接执行用户提供的代码 定期更新和维护您的自定义代码 第三章:核心验证功能实现 3.1 基础验证类设计 我们将创建一个核心验证类,包含常用的验证方法: class Smart_Form_Validator { private $errors = array(); private $cleaned_data = array(); /** * 验证电子邮件地址 */ public function validate_email($email, $field_name = 'email') { if (empty($email)) { $this->add_error($field_name, __('电子邮件地址不能为空', 'smart-form-validation')); return false; } // 使用WordPress内置函数验证邮箱格式 if (!is_email($email)) { $this->add_error($field_name, __('请输入有效的电子邮件地址', 'smart-form-validation')); return false; } // 检查邮箱域名是否真实存在 if (apply_filters('smart_form_check_email_domain', true)) { list($user, $domain) = explode('@', $email); if (!checkdnsrr($domain, 'MX')) { $this->add_error($field_name, __('电子邮件域名无效', 'smart-form-validation')); return false; } } $this->cleaned_data[$field_name] = sanitize_email($email); return true; } /** * 验证电话号码 */ public function validate_phone($phone, $field_name = 'phone', $country_code = 'CN') { $phone = preg_replace('/s+/', '', $phone); // 中国手机号验证 if ($country_code === 'CN') { if (!preg_match('/^1[3-9]d{9}$/', $phone)) { $this->add_error($field_name, __('请输入有效的中国手机号码', 'smart-form-validation')); return false; } } // 国际电话验证(简化版) else { if (!preg_match('/^+?[1-9]d{1,14}$/', $phone)) { $this->add_error($field_name, __('请输入有效的国际电话号码', 'smart-form-validation')); return false; } } $this->cleaned_data[$field_name] = sanitize_text_field($phone); return true; } /** * 验证URL */ public function validate_url($url, $field_name = 'url') { if (empty($url)) { return true; // URL字段可为空 } // 检查URL格式 if (!filter_var($url, FILTER_VALIDATE_URL)) { $this->add_error($field_name, __('请输入有效的URL地址', 'smart-form-validation')); return false; } // 检查URL是否安全 $parsed_url = parse_url($url); $blacklist_domains = apply_filters('smart_form_url_blacklist', array()); if (in_array($parsed_url['host'], $blacklist_domains)) { $this->add_error($field_name, __('该URL已被列入黑名单', 'smart-form-validation')); return false; } $this->cleaned_data[$field_name] = esc_url_raw($url); return true; } /** * 添加错误信息 */ private function add_error($field, $message) { $this->errors[$field] = $message; } /** * 获取所有错误 */ public function get_errors() { return $this->errors; } /** * 获取清洗后的数据 */ public function get_cleaned_data() { return $this->cleaned_data; } /** * 检查是否有错误 */ public function has_errors() { return !empty($this->errors); } } 3.2 高级验证功能实现 除了基础验证,我们还需要实现更智能的验证功能: /** * 智能文本内容验证 */ public function validate_smart_text($text, $field_name, $options = array()) { $defaults = array( 'min_length' => 0, 'max_length' => 0, 'allow_html' => false, 'block_spam_keywords' => true, 'check_duplicate' => false, ); $options = wp_parse_args($options, $defaults); // 检查长度 $text_length = mb_strlen($text, 'UTF-8'); if ($options['min_length'] > 0 && $text_length < $options['min_length']) { $this->add_error($field_name, sprintf(__('内容至少需要%d个字符', 'smart-form-validation'), $options['min_length'])); return false; } if ($options['max_length'] > 0 && $text_length > $options['max_length']) { $this->add_error($field_name, sprintf(__('内容不能超过%d个字符', 'smart-form-validation'), $options['max_length'])); return false; } // 垃圾关键词检测 if ($options['block_spam_keywords']) { $spam_keywords = $this->get_spam_keywords(); foreach ($spam_keywords as $keyword) { if (stripos($text, $keyword) !== false) { $this->add_error($field_name, __('内容包含不被允许的关键词', 'smart-form-validation')); return false; } } } // 重复内容检测 if ($options['check_duplicate']) { $hash = md5($text); $existing_hashes = get_option('smart_form_submission_hashes', array()); // 清理过期的哈希值(24小时前的提交) $one_day_ago = time() - 86400; foreach ($existing_hashes as $timestamp => $content_hash) { if ($timestamp < $one_day_ago) { unset($existing_hashes[$timestamp]); } } if (in_array($hash, $existing_hashes)) { $this->add_error($field_name, __('检测到重复提交的内容', 'smart-form-validation')); return false; } // 存储当前提交的哈希值 $existing_hashes[time()] = $hash; update_option('smart_form_submission_hashes', $existing_hashes); } // 数据清洗 if ($options['allow_html']) { // 允许有限的HTML标签 $allowed_tags = wp_kses_allowed_html('post'); $cleaned_text = wp_kses($text, $allowed_tags); } else { // 完全清除HTML $cleaned_text = sanitize_textarea_field($text); } $this->cleaned_data[$field_name] = $cleaned_text; return true; } /** * 获取垃圾关键词列表 */ private function get_spam_keywords() { $default_keywords = array( 'viagra', 'cialis', 'casino', 'loan', 'mortgage', '赚钱', '赌博', '色情', '代开发票', '信用卡套现' ); return apply_filters('smart_form_spam_keywords', $default_keywords); } 3.3 实时AJAX验证 为了提高用户体验,我们可以添加实时验证功能: /** * 注册AJAX验证端点 */ public function register_ajax_handlers() { add_action('wp_ajax_smart_form_validate_field', array($this, 'ajax_validate_field')); add_action('wp_ajax_nopriv_smart_form_validate_field', array($this, 'ajax_validate_field')); } /** * AJAX字段验证处理 */ public function ajax_validate_field() { // 安全检查 check_ajax_referer('smart_form_validation_nonce', 'nonce'); $field_name = sanitize_text_field($_POST['field_name'] ?? ''); $field_value = $_POST['field_value'] ?? ''; $field_type = sanitize_text_field($_POST['field_type'] ?? 'text'); $validator = new Smart_Form_Validator(); $is_valid = false; $message = ''; switch ($field_type) { case 'email': $is_valid = $validator->validate_email($field_value, $field_name); break; case 'phone': $is_valid = $validator->validate_phone($field_value, $field_name); break; case 'url': $is_valid = $validator->validate_url($field_value, $field_name); break; default: $is_valid = true; } if (!$is_valid) { $errors = $validator->get_errors(); $message = $errors[$field_name] ?? __('字段验证失败', 'smart-form-validation'); } wp_send_json(array( 'valid' => $is_valid, 'message' => $message, 'cleaned_value' => $validator->get_cleaned_data()[$field_name] ?? $field_value )); } 第四章:数据清洗与安全防护 4.1 深度数据清洗 数据清洗不仅仅是去除危险字符,还包括标准化和规范化: /** * 深度数据清洗类 */ class Smart_Data_Sanitizer { /** * 清洗用户输入数组 */ public static function sanitize_array($data, $rules = array()) { $sanitized = array(); foreach ($data as $key => $value) { $rule = $rules[$key] ?? 'text'; if (is_array($value)) { $sanitized[$key] = self::sanitize_array($value, $rules); } else { $sanitized[$key] = self::sanitize_field($value, $rule); } } return $sanitized; } /** * 根据规则清洗单个字段 */ public static function sanitize_field($value, $rule = 'text') { switch ($rule) { case 'email': return sanitize_email($value); case 'url': return esc_url_raw($value); case 'textarea': return sanitize_textarea_field($value); case 'html': $allowed_tags = wp_kses_allowed_html('post'); return wp_kses($value, $allowed_tags); case 'integer': return intval($value); case 'float': return floatval($value); case 'date': // 尝试解析日期并格式化为Y-m-d $timestamp = strtotime($value); return $timestamp ? date('Y-m-d', $timestamp) : ''; case 'phone': // 移除所有非数字字符,除了开头的+ $cleaned = preg_replace('/[^d+]/', '', $value); return substr($cleaned, 0, 20); // 限制长度 case 'credit_card': // 只保留数字,并添加掩码 $cleaned = preg_replace('/D/', '', $value); if (strlen($cleaned) >= 4) { return '**** **** **** ' . substr($cleaned, -4); } return $cleaned; case 'price': // 格式化价格,保留两位小数 $cleaned = preg_replace('/[^d.]/', '', $value); $float_value = floatval($cleaned); return number_format($float_value, 2, '.', ''); default: return sanitize_text_field($value); } } /** * 防止SQL注入 */ public static function prevent_sql_injection($input) { global $wpdb; // 使用$wpdb->prepare进行查询参数化 // 这里我们主要进行输入验证 $dangerous_patterns = array( '/unions+select/i', '/inserts+into/i', '/updates+.+set/i', '/deletes+from/i', '/drops+table/i', '/--/', '/#/', '//*/', '/*//', '/waitfors+delay/i', '/benchmark(/i' ); foreach ($dangerous_patterns as $pattern) { if (preg_match($pattern, $input)) { return ''; // 发现危险内容,返回空字符串 } } return $input; } /** * 防止XSS攻击 */ public static function prevent_xss($input) { // 移除危险的HTML属性和事件处理器 $dangerous_attributes = array( 'onload', 'onerror', 'onclick', 'onmouseover', 'onmouseout', 'onkeydown', 'onkeypress', 'onkeyup', 'javascript:', 'vbscript:', 'expression(' ); foreach ($dangerous_attributes as $attr) { $input = preg_replace('/' . preg_quote($attr, '/') . 's*:/i', 'blocked:', $input); $input = preg_replace('/' . preg_quote($attr, '/') . 's*=/i', 'blocked=', $input); } return $input; } } 4.2 垃圾信息检测与防护 /** * 垃圾信息检测类 */ class Smart_Spam_Detector { private $score = 0; private $threshold = 5; // 超过此分数视为垃圾信息 /** * 检测提交是否为垃圾信息 */ public function is_spam($data) { $this->score = 0; // 检查提交频率 $this->check_submission_frequency(); // 检查隐藏蜜罐字段 $this->check_honeypot($data); // 检查内容特征 if (isset($data['message'])) { $this->check_content_features($data['message']); } // 检查链接数量 $this->check_link_count($data); // 检查用户代理 $this->check_user_agent(); // 允许其他插件修改分数 $this->score = apply_filters('smart_form_spam_score', $this->score, $data); return $this->score >= $this->threshold; } /** * 检查提交频率 */ private function check_submission_frequency() { $ip = $this->get_user_ip(); $transient_key = 'smart_form_submission_' . md5($ip); $submission_count = get_transient($transient_key); if ($submission_count === false) { $submission_count = 0; set_transient($transient_key, 1, 300); // 5分钟限制 } else { $submission_count++; set_transient($transient_key, $submission_count, 300); // 短时间内多次提交增加垃圾分数 if ($submission_count > 3) { $this->score += ($submission_count - 2) * 2; } } } /** * 检查蜜罐字段 */ private function check_honeypot($data) { // 蜜罐字段名,对用户不可见 $honeypot_field = apply_filters('smart_form_honeypot_field', 'website_url'); if (isset($data[$honeypot_field]) && !empty($data[$honeypot_field])) { // 蜜罐字段被填写,很可能是机器人 $this->score += 10; } } /** * 检查内容特征 */ private function check_content_features($content) { // 检查大写字母比例 $total_chars = strlen($content); $uppercase_chars = preg_match_all('/[A-Z]/', $content); if ($total_chars > 10) { $uppercase_ratio = $uppercase_chars / $total_chars; if ($uppercase_ratio > 0.5) { $this->score += 3; // 过多大写字母 } } // 检查垃圾关键词 $spam_keywords = array( 'viagra', 'cialis', 'casino', 'loan', 'mortgage', '赚钱', '赌博', '色情', '代开发票', '信用卡套现' ); foreach ($spam_keywords as $keyword) { if (stripos($content, $keyword) !== false) { $this->score += 5; break; } } // 检查无意义重复 if ($this->has_repetitive_pattern($content)) { $this->score += 4; } } /** * 检查链接数量 */ private function check_link_count($data) { $total_links = 0; foreach ($data as $value) { if (is_string($value)) { // 简单统计链接数量 $links = preg_match_all('/https?://[^s]+/', $value, $matches); $total_links += $links; } } if ($total_links > 2) { $this->score += $total_links; // 每个链接加1分 } } /** * 检查用户代理 */ private function check_user_agent() { $user_agent = $_SERVER['HTTP_USER_AGENT'] ?? ''; if (empty($user_agent)) { $this->score += 3; // 空用户代理 return; } // 已知的垃圾机器人用户代理 $spam_bots = array( 'bot', 'crawler', 'spider', 'scraper', 'curl', 'wget', 'python-requests', 'java' ); foreach ($spam_bots as $bot) { if (stripos($user_agent, $bot) !== false) { $this->score += 2; break; } } } /** * 获取用户IP */ private function get_user_ip() { $ip = ''; if (!empty($_SERVER['HTTP_CLIENT_IP'])) { $ip = $_SERVER['HTTP_CLIENT_IP']; } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { $ip = $_SERVER['HTTP_X_FORWARDED_FOR']; } else { $ip = $_SERVER['REMOTE_ADDR'] ?? ''; } return sanitize_text_field($ip); } /** * 检查是否有重复模式 */ private function has_repetitive_pattern($content) { // 检查连续重复的单词 if (preg_match('/(bw+b)(?:s+1){2,}/i', $content)) { return true; } // 检查重复字符 if (preg_match('/(.)1{5,}/', $content)) { return true; } return false; } } 第五章:集成常用互联网小工具功能 5.1 地址智能补全与验证 /** * 地址智能处理类 */ class Smart_Address_Processor { /** * 集成第三方地址API进行验证 */ public static function validate_address($address, $country = 'CN') { $result = array( 'valid' => false, 'normalized' => '', 'components' => array(), 'suggestions' => array() ); // 基础格式验证 if (empty($address) || strlen($address) < 5) { return $result; } // 使用百度地图API进行地址验证(示例) if ($country === 'CN' && defined('SMART_FORM_BAIDU_MAP_AK')) { $api_result = self::validate_via_baidu_map($address); if ($api_result) { return $api_result; } } // 本地规则验证 return self::validate_locally($address, $country); } /** * 通过百度地图API验证地址 */ private static function validate_via_baidu_map($address) { $api_key = SMART_FORM_BAIDU_MAP_AK; $url = "http://api.map.baidu.com/geocoding/v3/?address=" . urlencode($address) . "&output=json&ak=" . $api_key; $response = wp_remote_get($url, array('timeout' => 5)); if (is_wp_error($response)) { return false; } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if ($data && $data['status'] == 0) { return array( 'valid' => true, 'normalized' => $data['result']['formatted_address'] ?? $address, 'components' => array( 'province' => $data['result']['addressComponent']['province'] ?? '', 'city' => $data['result']['addressComponent']['city'] ?? '', 'district' => $data['result']['addressComponent']['district'] ?? '', 'street' => $data['result']['addressComponent']['street'] ?? '' ), 'location' => array( 'lng' => $data['result']['location']['lng'] ?? '', 'lat' => $data['result']['location']['lat'] ?? '' ) ); } return false; } /** * 本地地址验证规则 */ private static function validate_locally($address, $country) { $result = array( 'valid' => false, 'normalized' => $address, 'components' => array() ); // 中国地址验证规则 if ($country === 'CN') { // 检查是否包含必要的地址元素 $required_elements = array('省', '市', '区', '路', '街', '号'); $found_elements = 0; foreach ($required_elements as $element) { if (mb_strpos($address, $element) !== false) { $found_elements++; } } $result['valid'] = $found_elements >= 2; // 尝试提取地址组件 if (preg_match('/(.*?[省市])(.*?[市区县])(.*?[路街道])(.*)/', $address, $matches)) { $result['components'] = array( 'province' => $matches[1] ?? '', 'city' => $matches[2] ?? '', 'district' => $matches[3] ?? '', 'detail' => $matches[4] ?? '' ); } } return $result; } /** * 地址自动补全 */ public static function autocomplete_address($input, $country = 'CN') { $suggestions = array(); // 这里可以集成第三方地址补全API // 示例:使用本地地址数据库 $address_database = get_option('smart_form_address_db', array()); if (!empty($address_database)) { foreach ($address_database as $address) { if (stripos($address, $input) !== false) { $suggestions[] = $address; if (count($suggestions) >= 5) { break; } } } } return apply_filters('smart_form_address_suggestions', $suggestions, $input, $country); } } 5.2 身份证号码验证工具 /** * 身份证验证类 */ class Smart_ID_Validator { /** * 验证中国身份证号码 */ public static function validate_chinese_id($id_number) { $id_number = strtoupper(trim($id_number)); // 基本格式验证 if (!preg_match('/^d{17}[dX]$/', $id_number)) { return array( 'valid' => false, 'error' => '身份证格式不正确', 'details' => array() ); } // 验证校验码 if (!self::verify_check_code($id_number)) { return array( 'valid' => false, 'error' => '身份证校验码错误', 'details' => array() ); } // 提取信息 $details = self::extract_id_info($id_number); // 验证出生日期 if (!checkdate($details['month'], $details['day'], $details['year'])) { return array( 'valid' => false, 'error' => '身份证出生日期无效', 'details' => $details ); } // 验证地区代码(前6位) if (!self::validate_region_code(substr($id_number, 0, 6))) { return array( 'valid' => false, 'error' => '身份证地区代码无效', 'details' => $details ); } return array( 'valid' => true, 'error' => '', 'details' => $details ); } /** * 验证校验码 */ private static function verify_check_code($id_number) { if (strlen($id_number) != 18) { return false; } // 加权因子 $weight_factors = array(7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2); // 校验码对应值 $check_codes = array('1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'); $sum = 0; for ($i = 0; $i < 17; $i++) { $sum += intval($id_number[$i]) * $weight_factors[$i]; } $mod = $sum % 11; $check_code = $check_codes[$mod]; return $check_code == $id_number[17]; } /** * 从身份证提取信息 */ private static function extract_id_info($id_number) { // 地区代码 $region_code = substr($id_number, 0, 6); // 出生日期 $birth_year = substr($id_number, 6, 4); $birth_month = substr($id_number, 10, 2); $birth_day = substr($id_number, 12, 2); // 顺序码 $sequence_code = substr($id_number, 14, 3); // 性别(顺序码奇数为男,偶数为女) $gender_code = intval($sequence_code); $gender = ($gender_code % 2 == 1) ? '男' : '女'; // 年龄计算 $current_year = date('Y'); $age = $current_year - $birth_year; // 判断是否已过生日 $current_month = date('m'); $current_day = date('d'); if ($current_month < $birth_month || ($current_month == $birth_month && $current_day < $birth_day)) { $age--; } return array( 'region_code' => $region_code, 'birth_date' => $birth_year . '-' . $birth_month . '-' . $birth_day, 'year' => intval($birth_year), 'month' => intval($birth_month), 'day' => intval($birth_day), 'sequence_code' => $sequence_code, 'gender' => $gender, 'age' => $age ); } /** * 验证地区代码 */ private static function validate_region_code($region_code) { // 这里可以集成地区代码数据库 // 简化版:只验证基本格式 $valid_prefixes = array( '11', '12', '13', '14', '15', // 华北 '21', '22', '23', // 东北 '31', '32', '33', '34', // 华东 '35', '36', '37', // 华中 '41', '42', '43', '44', '45', '46', // 华南 '50', '51', '52', '53', '54', // 西南 '61', '62', '63', '64', '65', // 西北 '71', '81', '82' // 港澳台 ); $prefix = substr($region_code, 0, 2); return in_array($prefix, $valid_prefixes); } } 5.3 银行卡号验证与识别 /** * 银行卡验证类 */ class Smart_Bankcard_Validator { /** * 验证银行卡号 */ public static function validate_bankcard($card_number) { $card_number = preg_replace('/s+/', '', $card_number); // 基本格式验证 if (!preg_match('/^d{13,19}$/', $card_number)) { return array( 'valid' => false, 'error' => '银行卡号格式不正确', 'details' => array() ); } // Luhn算法验证 if (!self::validate_luhn($card_number)) { return array( 'valid' => false, 'error' => '银行卡号校验失败', 'details' => array() ); } // 识别银行和卡类型 $details = self::identify_bankcard($card_number); return array( 'valid' => true, 'error' => '', 'details' => $details ); } /** * Luhn算法验证 */ private static function validate_luhn($card_number) { $sum = 0; $length = strlen($card_number); $parity = $length % 2; for ($i = 0; $i < $length; $i++) { $digit = intval($card_number[$i]); if ($i % 2 == $parity) { $digit *= 2; if ($digit > 9) { $digit -= 9; } } $sum += $digit; } return ($sum % 10) == 0; } /** * 识别银行卡信息 */ private static function identify_bankcard($card_number) { $bin_codes = array( // 借记卡 '622848' => array('bank' => '农业银行', 'type' => '借记卡'), '622700' => array('bank' => '建设银行', 'type' => '借记卡'), '622262' => array('bank' => '交通银行', 'type' => '借记卡'), '622588' => array('bank' => '招商银行', 'type' => '借记卡'), '622760' => array('bank' => '中国银行', 'type' => '借记卡'), '622202' => array('bank' => '工商银行', 'type' => '借记卡'), // 信用卡 '438088' => array('bank' => '建设银行', 'type' => '信用卡'), '518710' => array('bank' => '招商银行', 'type' => '信用卡'), '622155' => array('bank' => '工商银行', 'type' => '信用卡'), '622156' => array('bank' => '工商银行', 'type' => '信用卡'), // 更多BIN码可以继续添加 ); $details = array( 'bank' => '未知', 'type' => '未知', 'length' => strlen($card_number), 'bin' => substr($card_number, 0, 6) ); // 检查前6位BIN码 foreach ($bin_codes as $bin => $info) {
发表评论详细指南:开发网站会员每日签到与连续登录奖励积分体系 摘要 在当今互联网时代,用户参与度和忠诚度是网站成功的关键因素之一。每日签到与连续登录奖励体系作为一种有效的用户激励策略,已被广泛应用于各类网站和应用程序中。本文将详细介绍如何通过WordPress程序的代码二次开发,实现一个功能完善的会员每日签到与连续登录奖励积分体系。我们将从系统设计、数据库结构、前端界面到后端逻辑进行全面讲解,帮助您打造一个能够提升用户活跃度的实用工具。 一、系统需求分析与设计 1.1 功能需求分析 在开始开发之前,我们需要明确系统应具备的核心功能: 每日签到功能:用户每天可进行一次签到,获得基础积分奖励 连续登录奖励:根据用户连续登录天数提供递增的积分奖励 签到日历展示:直观显示用户本月签到情况 积分记录查询:用户可查看自己的积分获取和消费记录 奖励规则配置:管理员可灵活配置签到奖励规则 用户等级体系:根据积分或连续签到天数划分用户等级 断签处理机制:定义连续签到中断后的处理规则 1.2 技术架构设计 本系统将基于WordPress平台开发,主要技术栈包括: 前端:HTML5、CSS3、JavaScript(jQuery)、AJAX 后端:PHP(WordPress核心API) 数据库:MySQL(WordPress数据库) 安全机制:WordPress非ces、数据验证与清理 1.3 数据库设计 我们需要在WordPress现有数据库结构基础上,添加以下自定义表: -- 用户签到记录表 CREATE TABLE wp_signin_records ( id INT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT(20) NOT NULL, sign_date DATE NOT NULL, sign_time DATETIME NOT NULL, points_earned INT NOT NULL, continuous_days INT NOT NULL, UNIQUE KEY user_date (user_id, sign_date) ); -- 用户积分总表 CREATE TABLE wp_user_points ( id INT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT(20) NOT NULL UNIQUE, total_points INT DEFAULT 0, current_points INT DEFAULT 0, last_sign_date DATE, continuous_days INT DEFAULT 0, max_continuous_days INT DEFAULT 0 ); -- 积分变动记录表 CREATE TABLE wp_points_log ( id INT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT(20) NOT NULL, points_change INT NOT NULL, change_type VARCHAR(50) NOT NULL, change_reason VARCHAR(255), change_time DATETIME NOT NULL, related_id BIGINT(20) ); 二、开发环境搭建与准备工作 2.1 开发环境配置 本地开发环境:安装XAMPP/MAMP或Local by Flywheel WordPress安装:下载最新版WordPress并完成基本配置 代码编辑器:推荐使用VS Code、PHPStorm或Sublime Text 浏览器开发者工具:用于调试前端代码 2.2 创建WordPress插件 我们将以插件形式实现签到功能,确保与主题的独立性: 在wp-content/plugins/目录下创建新文件夹daily-signin-rewards 创建主插件文件daily-signin-rewards.php: <?php /** * Plugin Name: 每日签到与连续登录奖励系统 * Plugin URI: https://yourwebsite.com/ * Description: 为WordPress网站添加会员每日签到与连续登录奖励积分功能 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later * Text Domain: daily-signin */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('DSR_VERSION', '1.0.0'); define('DSR_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('DSR_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 require_once DSR_PLUGIN_DIR . 'includes/class-dsr-core.php'; function dsr_init() { $plugin = new DSR_Core(); $plugin->run(); } add_action('plugins_loaded', 'dsr_init'); 2.3 创建数据库表 在插件激活时创建所需数据库表: // 在class-dsr-core.php中添加 class DSR_Core { // ... 其他代码 public function activate() { $this->create_tables(); $this->set_default_options(); } private function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); // 创建签到记录表 $table_name = $wpdb->prefix . 'signin_records'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id INT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT(20) NOT NULL, sign_date DATE NOT NULL, sign_time DATETIME NOT NULL, points_earned INT NOT NULL, continuous_days INT NOT NULL, UNIQUE KEY user_date (user_id, sign_date) ) $charset_collate;"; // 创建其他表... require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } } 三、核心功能模块开发 3.1 用户签到功能实现 3.1.1 签到逻辑处理 class DSR_Signin_Handler { // 处理用户签到请求 public function process_signin($user_id) { // 检查用户是否已签到 if ($this->has_signed_today($user_id)) { return array( 'success' => false, 'message' => '您今天已经签到过了,请明天再来!' ); } // 获取连续签到天数 $continuous_days = $this->calculate_continuous_days($user_id); $continuous_days++; // 计算本次签到应得积分 $points = $this->calculate_points($continuous_days); // 记录签到 $this->record_signin($user_id, $points, $continuous_days); // 更新用户积分 $this->update_user_points($user_id, $points, $continuous_days); // 记录积分变动 $this->log_points_change($user_id, $points, 'daily_signin', '每日签到奖励'); return array( 'success' => true, 'points' => $points, 'continuous_days' => $continuous_days, 'message' => sprintf('签到成功!获得%d积分,已连续签到%d天', $points, $continuous_days) ); } // 检查用户今天是否已签到 private function has_signed_today($user_id) { global $wpdb; $table_name = $wpdb->prefix . 'signin_records'; $today = current_time('Y-m-d'); $count = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $table_name WHERE user_id = %d AND sign_date = %s", $user_id, $today )); return $count > 0; } // 计算连续签到天数 private function calculate_continuous_days($user_id) { global $wpdb; $table_name = $wpdb->prefix . 'user_points'; $result = $wpdb->get_row($wpdb->prepare( "SELECT last_sign_date, continuous_days FROM $table_name WHERE user_id = %d", $user_id )); if (!$result) { return 0; } $last_sign_date = $result->last_sign_date; $yesterday = date('Y-m-d', strtotime('-1 day')); // 如果昨天签到过,连续天数加1,否则重置为0 if ($last_sign_date == $yesterday) { return $result->continuous_days; } else { return 0; } } // 计算签到积分(可根据连续天数递增) private function calculate_points($continuous_days) { $base_points = get_option('dsr_base_points', 10); // 连续签到奖励规则 $continuous_bonus = 0; if ($continuous_days >= 7) { $continuous_bonus = get_option('dsr_week_bonus', 20); } elseif ($continuous_days >= 30) { $continuous_bonus = get_option('dsr_month_bonus', 50); } return $base_points + $continuous_bonus; } } 3.1.2 AJAX签到接口 // 添加AJAX处理钩子 add_action('wp_ajax_dsr_signin', 'dsr_ajax_signin'); add_action('wp_ajax_nopriv_dsr_signin', 'dsr_ajax_signin_nopriv'); function dsr_ajax_signin() { // 安全检查 check_ajax_referer('dsr_signin_nonce', 'nonce'); // 验证用户登录状态 if (!is_user_logged_in()) { wp_send_json_error(array('message' => '请先登录')); return; } $user_id = get_current_user_id(); $signin_handler = new DSR_Signin_Handler(); $result = $signin_handler->process_signin($user_id); if ($result['success']) { wp_send_json_success($result); } else { wp_send_json_error(array('message' => $result['message'])); } } function dsr_ajax_signin_nopriv() { wp_send_json_error(array('message' => '请先登录')); } 3.2 签到界面与交互设计 3.2.1 前端签到组件 <!-- 在插件中创建签到界面模板 --> <div class="dsr-signin-container"> <div class="dsr-signin-header"> <h3>每日签到</h3> <p class="dsr-user-stats"> 当前积分: <span id="dsr-current-points">0</span> | 连续签到: <span id="dsr-continuous-days">0</span>天 </p> </div> <div class="dsr-signin-calendar"> <div class="dsr-calendar-header"> <button class="dsr-prev-month"><</button> <h4 id="dsr-current-month">2023年10月</h4> <button class="dsr-next-month">></button> </div> <div class="dsr-calendar-days"> <!-- 通过JavaScript动态生成日历 --> </div> </div> <div class="dsr-signin-action"> <button id="dsr-signin-btn" class="dsr-signin-button"> <span class="dsr-button-text">立即签到</span> <span class="dsr-button-points">+10积分</span> </button> <p class="dsr-signin-tip" id="dsr-signin-status">今日尚未签到</p> </div> <div class="dsr-rewards-rules"> <h4>签到规则</h4> <ul> <li>每日签到可获得10基础积分</li> <li>连续签到7天额外奖励20积分</li> <li>连续签到30天额外奖励50积分</li> <li>中断签到后连续天数将重新计算</li> </ul> </div> </div> 3.2.2 日历生成与签到状态显示 // 前端JavaScript代码 jQuery(document).ready(function($) { // 初始化签到系统 function initSigninSystem() { // 获取用户签到数据 $.ajax({ url: dsr_ajax.ajax_url, type: 'POST', data: { action: 'dsr_get_signin_data', nonce: dsr_ajax.nonce }, success: function(response) { if (response.success) { updateUserStats(response.data); renderCalendar(response.data.calendar); updateSigninButton(response.data.signed_today); } } }); // 绑定签到按钮事件 $('#dsr-signin-btn').on('click', function() { if ($(this).hasClass('signed')) { return; } $.ajax({ url: dsr_ajax.ajax_url, type: 'POST', data: { action: 'dsr_signin', nonce: dsr_ajax.signin_nonce }, success: function(response) { if (response.success) { showSigninSuccess(response.data); } else { alert(response.data.message); } } }); }); } // 渲染签到日历 function renderCalendar(calendarData) { const $calendar = $('.dsr-calendar-days'); $calendar.empty(); // 生成日历标题行(星期) const weekdays = ['日', '一', '二', '三', '四', '五', '六']; weekdays.forEach(day => { $calendar.append(`<div class="dsr-weekday">${day}</div>`); }); // 生成日期格子 calendarData.days.forEach(day => { let dayClass = 'dsr-calendar-day'; if (day.is_today) dayClass += ' dsr-today'; if (day.signed) dayClass += ' dsr-signed'; if (!day.in_month) dayClass += ' dsr-other-month'; const dayHtml = ` <div class="${dayClass}" data-date="${day.date}"> <div class="dsr-day-number">${day.day}</div> ${day.signed ? '<div class="dsr-signed-icon">✓</div>' : ''} ${day.points ? `<div class="dsr-day-points">+${day.points}</div>` : ''} </div> `; $calendar.append(dayHtml); }); } // 显示签到成功效果 function showSigninSuccess(data) { const $btn = $('#dsr-signin-btn'); $btn.addClass('signed').html('今日已签到'); $('#dsr-signin-status').text('签到成功!'); // 显示积分动画 const $points = $('#dsr-current-points'); const currentPoints = parseInt($points.text()); const newPoints = currentPoints + data.points; animatePoints(currentPoints, newPoints, $points); $('#dsr-continuous-days').text(data.continuous_days); // 显示签到成功提示 const $tip = $('<div class="dsr-success-tip">签到成功!+' + data.points + '积分</div>'); $('.dsr-signin-action').append($tip); setTimeout(() => $tip.fadeOut(), 3000); } // 初始化 initSigninSystem(); }); 3.3 积分管理与等级系统 3.3.1 用户等级计算 class DSR_Level_System { // 根据积分计算用户等级 public function calculate_level($points) { $levels = $this->get_levels_config(); foreach ($levels as $level => $requirement) { if ($points >= $requirement['min_points']) { $current_level = $level; } else { break; } } return isset($current_level) ? $current_level : 1; } // 获取等级配置 private function get_levels_config() { return array( 1 => array('name' => '新手', 'min_points' => 0, 'icon' => '⭐'), 2 => array('name' => '青铜', 'min_points' => 100, 'icon' => '🥉'), 3 => array('name' => '白银', 'min_points' => 500, 'icon' => '🥈'), 4 => array('name' => '黄金', 'min_points' => 2000, 'icon' => '🥇'), 5 => array('name' => '铂金', 'min_points' => 5000, 'icon' => '💎'), 6 => array('name' => '钻石', 'min_points' => 10000, 'icon' => '👑'), 7 => array('name' => '至尊', 'min_points' => 20000, 'icon' => '🏆') ); } // 获取用户等级信息 public function get_user_level_info($user_id) { $points = $this->get_user_points($user_id); $level = $this->calculate_level($points); $levels = $this->get_levels_config(); $current_level_info = $levels[$level]; $next_level = $level + 1; $next_level_info = isset($levels[$next_level]) ? $levels[$next_level] : null; return array( 'level' => $level, 'level_name' => $current_level_info['name'], 'level_icon' => $current_level_info['icon'], 'current_points' => $points, 'next_level_points' => $next_level_info ? $next_level_info['min_points'] : null, 'points_to_next' => $next_level_info ? $next_level_info['min_points'] - $points : 0, 'progress_percentage' => $next_level_info ? min(100, round(($points / $next_level_info['min_points']) * 100, 2)) : 100 ); } } 3.3 3.3.2 积分消费与兑换功能 class DSR_Points_Exchange { // 处理积分兑换请求 public function process_exchange($user_id, $item_id, $quantity = 1) { // 获取兑换物品信息 $exchange_item = $this->get_exchange_item($item_id); if (!$exchange_item) { return array( 'success' => false, 'message' => '兑换物品不存在' ); } // 检查库存 if ($exchange_item['stock'] !== -1 && $exchange_item['stock'] < $quantity) { return array( 'success' => false, 'message' => '库存不足' ); } // 计算所需总积分 $total_points_needed = $exchange_item['points'] * $quantity; // 检查用户积分是否足够 $user_points = $this->get_user_available_points($user_id); if ($user_points < $total_points_needed) { return array( 'success' => false, 'message' => '积分不足,还需要' . ($total_points_needed - $user_points) . '积分' ); } // 执行兑换 $exchange_id = $this->record_exchange($user_id, $item_id, $quantity, $total_points_needed); // 扣除积分 $this->deduct_points($user_id, $total_points_needed, $exchange_id); // 更新库存 if ($exchange_item['stock'] !== -1) { $this->update_item_stock($item_id, $quantity); } // 发送兑换成功通知 $this->send_exchange_notification($user_id, $exchange_item, $quantity); return array( 'success' => true, 'exchange_id' => $exchange_id, 'message' => '兑换成功!' . $exchange_item['name'] . '×' . $quantity ); } // 获取可兑换物品列表 public function get_exchange_items($category = 'all') { global $wpdb; $table_name = $wpdb->prefix . 'dsr_exchange_items'; $where = "status = 'publish'"; if ($category !== 'all') { $where .= $wpdb->prepare(" AND category = %s", $category); } $items = $wpdb->get_results( "SELECT * FROM $table_name WHERE $where ORDER BY sort_order ASC, id DESC", ARRAY_A ); return $items; } } 四、后台管理功能开发 4.1 管理菜单与界面 class DSR_Admin { public function __construct() { add_action('admin_menu', array($this, 'add_admin_menu')); add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts')); } // 添加管理菜单 public function add_admin_menu() { add_menu_page( '签到系统', '签到系统', 'manage_options', 'dsr-dashboard', array($this, 'render_dashboard'), 'dashicons-calendar-alt', 30 ); add_submenu_page( 'dsr-dashboard', '签到统计', '签到统计', 'manage_options', 'dsr-statistics', array($this, 'render_statistics') ); add_submenu_page( 'dsr-dashboard', '奖励设置', '奖励设置', 'manage_options', 'dsr-settings', array($this, 'render_settings') ); add_submenu_page( 'dsr-dashboard', '积分兑换', '积分兑换', 'manage_options', 'dsr-exchange', array($this, 'render_exchange') ); add_submenu_page( 'dsr-dashboard', '用户积分', '用户积分', 'manage_options', 'dsr-users', array($this, 'render_users') ); } // 渲染仪表盘 public function render_dashboard() { ?> <div class="wrap dsr-admin-dashboard"> <h1>签到系统仪表盘</h1> <div class="dsr-stats-cards"> <div class="dsr-stat-card"> <h3>今日签到人数</h3> <p class="dsr-stat-number"><?php echo $this->get_today_signin_count(); ?></p> </div> <div class="dsr-stat-card"> <h3>本月签到总数</h3> <p class="dsr-stat-number"><?php echo $this->get_month_signin_count(); ?></p> </div> <div class="dsr-stat-card"> <h3>总发放积分</h3> <p class="dsr-stat-number"><?php echo $this->get_total_points_given(); ?></p> </div> <div class="dsr-stat-card"> <h3>活跃用户数</h3> <p class="dsr-stat-number"><?php echo $this->get_active_users_count(); ?></p> </div> </div> <div class="dsr-charts-section"> <h2>最近30天签到趋势</h2> <canvas id="dsr-signin-chart" width="800" height="300"></canvas> </div> <div class="dsr-recent-activity"> <h2>最近签到记录</h2> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <th>用户</th> <th>签到时间</th> <th>获得积分</th> <th>连续天数</th> </tr> </thead> <tbody> <?php $this->display_recent_signins(); ?> </tbody> </table> </div> </div> <?php } } 4.2 奖励规则设置界面 // 奖励设置页面 public function render_settings() { // 保存设置 if (isset($_POST['submit']) && check_admin_referer('dsr_settings_save')) { $this->save_settings($_POST); echo '<div class="notice notice-success"><p>设置已保存!</p></div>'; } $settings = $this->get_settings(); ?> <div class="wrap"> <h1>签到奖励设置</h1> <form method="post" action=""> <?php wp_nonce_field('dsr_settings_save'); ?> <table class="form-table"> <tr> <th scope="row">基础积分</th> <td> <input type="number" name="base_points" value="<?php echo esc_attr($settings['base_points']); ?>" min="1" max="1000" /> <p class="description">用户每日签到可获得的基础积分</p> </td> </tr> <tr> <th scope="row">连续签到7天奖励</th> <td> <input type="number" name="week_bonus" value="<?php echo esc_attr($settings['week_bonus']); ?>" min="0" max="1000" /> <p class="description">连续签到7天时的额外奖励积分</p> </td> </tr> <tr> <th scope="row">连续签到30天奖励</th> <td> <input type="number" name="month_bonus" value="<?php echo esc_attr($settings['month_bonus']); ?>" min="0" max="5000" /> <p class="description">连续签到30天时的额外奖励积分</p> </td> </tr> <tr> <th scope="row">补签卡消耗积分</th> <td> <input type="number" name="re_sign_cost" value="<?php echo esc_attr($settings['re_sign_cost']); ?>" min="0" max="1000" /> <p class="description">使用补签卡需要消耗的积分</p> </td> </tr> <tr> <th scope="row">断签规则</th> <td> <select name="break_rule"> <option value="reset" <?php selected($settings['break_rule'], 'reset'); ?>> 完全重置 </option> <option value="keep_half" <?php selected($settings['break_rule'], 'keep_half'); ?>> 保留一半连续天数 </option> <option value="keep_max" <?php selected($settings['break_rule'], 'keep_max'); ?>> 保留历史最高记录 </option> </select> <p class="description">连续签到中断后的处理方式</p> </td> </tr> <tr> <th scope="row">特殊日期奖励</th> <td> <textarea name="special_dates" rows="5" cols="50"><?php echo esc_textarea($settings['special_dates']); ?></textarea> <p class="description">格式:日期=奖励积分,每行一个,如:01-01=100(元旦)</p> </td> </tr> </table> <?php submit_button('保存设置'); ?> </form> </div> <?php } 五、高级功能扩展 5.1 补签卡功能 class DSR_ReSign_Card { // 使用补签卡 public function use_resign_card($user_id, $target_date) { // 检查目标日期是否合法 if (!$this->is_valid_resign_date($target_date)) { return array( 'success' => false, 'message' => '该日期不可补签' ); } // 检查是否已签到 if ($this->has_signed_on_date($user_id, $target_date)) { return array( 'success' => false, 'message' => '该日期已签到' ); } // 检查补签卡数量 $card_count = $this->get_user_resign_card_count($user_id); if ($card_count < 1) { return array( 'success' => false, 'message' => '补签卡不足' ); } // 消耗补签卡 $this->deduct_resign_card($user_id, 1); // 执行补签 $signin_handler = new DSR_Signin_Handler(); $result = $signin_handler->process_resign($user_id, $target_date); if ($result['success']) { // 记录补签日志 $this->log_resign_usage($user_id, $target_date); return array( 'success' => true, 'message' => '补签成功!', 'data' => $result ); } return $result; } // 购买补签卡 public function buy_resign_card($user_id, $quantity = 1) { $card_price = get_option('dsr_resign_card_price', 100); $total_cost = $card_price * $quantity; // 检查用户积分 $user_points = $this->get_user_points($user_id); if ($user_points < $total_cost) { return array( 'success' => false, 'message' => '积分不足' ); } // 扣除积分 $this->deduct_points($user_id, $total_cost); // 增加补签卡 $this->add_resign_cards($user_id, $quantity); // 记录交易 $this->log_card_purchase($user_id, $quantity, $total_cost); return array( 'success' => true, 'message' => "成功购买{$quantity}张补签卡,消耗{$total_cost}积分" ); } } 5.2 签到提醒与推送 class DSR_Notifications { // 发送每日签到提醒 public function send_daily_reminder() { // 获取今天未签到的活跃用户 $users = $this->get_unsigned_active_users(); foreach ($users as $user) { // 发送站内信 $this->send_site_message($user->ID); // 发送邮件提醒(如果用户开启了邮件通知) if ($this->user_wants_email($user->ID)) { $this->send_email_reminder($user); } // 发送微信/APP推送(如果有集成) if ($this->has_wechat_integration()) { $this->send_wechat_push($user->ID); } } } // 发送连续签到成就通知 public function send_achievement_notification($user_id, $achievement_type, $data) { $user = get_userdata($user_id); $message = ''; switch ($achievement_type) { case 'continuous_7': $message = "恭喜您连续签到7天!获得额外奖励积分。"; break; case 'continuous_30': $message = "恭喜您连续签到30天!获得月度签到王称号。"; break; case 'total_points': $message = "恭喜您总积分达到{$data['points']}!升级为{$data['level']}。"; break; } // 添加站内通知 $this->add_site_notification($user_id, $message, 'achievement'); // 发送邮件 $this->send_achievement_email($user, $message); } } 5.3 社交分享激励 class DSR_Social_Share { // 处理社交分享奖励 public function process_share_reward($user_id, $platform, $content_type) { // 检查今日是否已分享 if ($this->has_shared_today($user_id, $platform)) { return array( 'success' => false, 'message' => '今日已通过此平台分享' ); } // 获取分享奖励积分 $points = $this->get_share_points($platform, $content_type); // 记录分享 $this->record_share($user_id, $platform, $content_type, $points); // 奖励积分 $this->add_points($user_id, $points, 'social_share', "{$platform}分享奖励"); return array( 'success' => true, 'points' => $points, 'message' => "分享成功!获得{$points}积分" ); } // 获取分享按钮HTML public function get_share_buttons($post_id = null) { $post_id = $post_id ?: get_the_ID(); $share_url = get_permalink($post_id); $title = get_the_title($post_id); $buttons = array( 'wechat' => array( 'name' => '微信', 'icon' => 'wechat', 'url' => 'javascript:;', 'class' => 'dsr-share-wechat', 'points' => 5 ), 'weibo' => array( 'name' => '微博', 'icon' => 'weibo', 'url' => "http://service.weibo.com/share/share.php?url={$share_url}&title={$title}", 'class' => 'dsr-share-weibo', 'points' => 10 ), 'qq' => array( 'name' => 'QQ', 'icon' => 'qq', 'url' => "https://connect.qq.com/widget/shareqq/index.html?url={$share_url}&title={$title}", 'class' => 'dsr-share-qq', 'points' => 8 ) ); ob_start(); ?> <div class="dsr-share-buttons"> <p>分享到社交网络可获得额外积分:</p> <div class="dsr-share-platforms"> <?php foreach ($buttons as $platform => $button): ?> <a href="<?php echo $button['url']; ?>" class="dsr-share-button <?php echo $button['class']; ?>" data-platform="<?php echo $platform; ?>" data-points="<?php echo $button['points']; ?>" target="_blank"> <span class="dsr-share-icon"><?php echo $button['icon']; ?></span> <span class="dsr-share-text"><?php echo $button['name']; ?></span> <span class="dsr-share-points">+<?php echo $button['points']; ?>积分</span> </a> <?php endforeach; ?> </div> </div> <?php return ob_get_clean(); } } 六、性能优化与安全加固 6.1 数据库查询优化 class DSR_Optimizer { // 使用缓存优化频繁查询 public function get_user_points_cached($user_id) { $cache_key = "dsr_user_points_{$user_id}"; $points = wp_cache_get($cache_key, 'dsr'); if (false === $points) { global $wpdb; $table_name = $wpdb->prefix . 'user_points'; $points = $wpdb->get_var($wpdb->prepare( "SELECT current_points FROM $table_name WHERE user_id = %d", $user_id )); if (is_null($points)) { $points = 0; }
发表评论一步步实现:为WordPress打造内嵌的在线流程图与UI原型设计工具 引言:为什么WordPress需要内置设计工具? 在当今数字化时代,网站不仅仅是信息展示平台,更是用户体验和交互设计的重要载体。对于WordPress用户而言,虽然市面上有众多第三方设计工具,但频繁切换平台、格式兼容性问题以及额外成本常常成为工作流程中的痛点。想象一下,如果能在WordPress编辑器中直接创建流程图、线框图和UI原型,将极大提升内容创作效率和协作便利性。 本文将通过详细的代码实现步骤,展示如何为WordPress开发一个内嵌的在线设计工具,让用户无需离开WordPress环境就能完成专业的设计工作。我们将从需求分析开始,逐步深入到架构设计、核心功能实现和优化方案,最终打造一个功能完善、性能优异的WordPress设计工具插件。 第一章:项目规划与需求分析 1.1 核心功能需求 在开始编码之前,我们需要明确工具的核心功能: 流程图绘制:支持基本形状、连接线、文本标注 UI原型设计:提供常用UI组件库(按钮、输入框、导航栏等) 实时协作:支持多用户同时编辑(可选高级功能) 导出功能:支持PNG、SVG、PDF格式导出 版本控制:设计稿的版本管理和回溯 与WordPress内容集成:可将设计直接插入文章或页面 1.2 技术选型与架构设计 考虑到工具需要在浏览器中运行,我们选择以下技术栈: 前端框架:React + TypeScript(提供良好的组件化开发和类型安全) 绘图库:Fabric.js 或 Konva.js(处理Canvas绘图操作) 后端:WordPress REST API + 自定义端点 数据存储:WordPress数据库自定义表 + 文件系统存储 实时协作:WebSocket(使用Pusher或Socket.io) 1.3 开发环境搭建 首先,我们需要设置WordPress插件开发环境: <?php /** * Plugin Name: WP Design Studio * Plugin URI: https://example.com/wp-design-studio * Description: 内嵌的在线流程图与UI原型设计工具 * Version: 1.0.0 * Author: Your Name * License: GPL v2 or later */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('WPDS_VERSION', '1.0.0'); define('WPDS_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('WPDS_PLUGIN_URL', plugin_dir_url(__FILE__)); 第二章:数据库设计与后端开发 2.1 创建自定义数据库表 我们需要存储设计项目的数据结构: // 在插件激活时创建数据库表 register_activation_hook(__FILE__, 'wpds_create_tables'); function wpds_create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'wpds_designs'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, title varchar(255) NOT NULL, type varchar(50) NOT NULL DEFAULT 'flowchart', content longtext NOT NULL, thumbnail varchar(500) DEFAULT NULL, settings text DEFAULT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY user_id (user_id), KEY type (type) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 创建版本历史表 $history_table = $wpdb->prefix . 'wpds_design_history'; $sql_history = "CREATE TABLE IF NOT EXISTS $history_table ( id bigint(20) NOT NULL AUTO_INCREMENT, design_id bigint(20) NOT NULL, version int(11) NOT NULL, content longtext NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, user_id bigint(20) NOT NULL, PRIMARY KEY (id), KEY design_id (design_id), KEY version (version) ) $charset_collate;"; dbDelta($sql_history); } 2.2 实现REST API端点 为前端提供数据交互接口: // 注册REST API路由 add_action('rest_api_init', 'wpds_register_rest_routes'); function wpds_register_rest_routes() { // 获取设计列表 register_rest_route('wpds/v1', '/designs', array( 'methods' => 'GET', 'callback' => 'wpds_get_designs', 'permission_callback' => function () { return current_user_can('edit_posts'); } )); // 获取单个设计 register_rest_route('wpds/v1', '/designs/(?P<id>d+)', array( 'methods' => 'GET', 'callback' => 'wpds_get_design', 'permission_callback' => function () { return current_user_can('edit_posts'); } )); // 创建/更新设计 register_rest_route('wpds/v1', '/designs', array( 'methods' => 'POST', 'callback' => 'wpds_save_design', 'permission_callback' => function () { return current_user_can('edit_posts'); } )); // 删除设计 register_rest_route('wpds/v1', '/designs/(?P<id>d+)', array( 'methods' => 'DELETE', 'callback' => 'wpds_delete_design', 'permission_callback' function () { return current_user_can('delete_posts'); } )); } // 获取设计列表的实现 function wpds_get_designs($request) { global $wpdb; $table_name = $wpdb->prefix . 'wpds_designs'; $user_id = get_current_user_id(); $page = $request->get_param('page') ? intval($request->get_param('page')) : 1; $per_page = $request->get_param('per_page') ? intval($request->get_param('per_page')) : 20; $offset = ($page - 1) * $per_page; $designs = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $table_name WHERE user_id = %d ORDER BY updated_at DESC LIMIT %d OFFSET %d", $user_id, $per_page, $offset ) ); $total = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $table_name WHERE user_id = %d", $user_id ) ); return new WP_REST_Response(array( 'designs' => $designs, 'pagination' => array( 'total' => $total, 'pages' => ceil($total / $per_page), 'current' => $page, 'per_page' => $per_page ) ), 200); } 第三章:前端架构与核心组件开发 3.1 设置React开发环境 由于WordPress传统开发方式与现代前端框架存在差异,我们需要特殊配置: // webpack.config.js const path = require('path'); const webpack = require('webpack'); module.exports = { entry: { 'wpds-editor': './src/editor/index.tsx', 'wpds-block': './src/block/index.tsx', }, output: { path: path.resolve(__dirname, 'assets/js'), filename: '[name].js', }, externals: { 'react': 'React', 'react-dom': 'ReactDOM', 'wp': 'wp', }, module: { rules: [ { test: /.(ts|tsx)$/, exclude: /node_modules/, use: 'ts-loader', }, { test: /.css$/, use: ['style-loader', 'css-loader'], }, ], }, resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'], }, plugins: [ new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), }), ], }; 3.2 核心编辑器组件 实现设计工具的核心画布组件: // src/editor/components/DesignCanvas.tsx import React, { useRef, useEffect, useState } from 'react'; import { FabricCanvas } from './FabricCanvas'; import { Toolbar } from './Toolbar'; import { PropertiesPanel } from './PropertiesPanel'; import { ShapesLibrary } from './ShapesLibrary'; import { saveDesign, loadDesign } from '../services/api'; interface DesignCanvasProps { designId?: number; onSave?: (design: any) => void; } export const DesignCanvas: React.FC<DesignCanvasProps> = ({ designId, onSave }) => { const canvasRef = useRef<any>(null); const [selectedTool, setSelectedTool] = useState<string>('select'); const [selectedObject, setSelectedObject] = useState<any>(null); const [designData, setDesignData] = useState<any>(null); const [isLoading, setIsLoading] = useState<boolean>(false); // 加载设计数据 useEffect(() => { if (designId) { loadDesignData(designId); } }, [designId]); const loadDesignData = async (id: number) => { setIsLoading(true); try { const data = await loadDesign(id); setDesignData(data); if (canvasRef.current) { canvasRef.current.loadFromJSON(data.content); } } catch (error) { console.error('Failed to load design:', error); } finally { setIsLoading(false); } }; const handleSave = async () => { if (!canvasRef.current) return; const content = canvasRef.current.toJSON(); const design = { title: designData?.title || '未命名设计', type: 'flowchart', content: JSON.stringify(content), settings: JSON.stringify({}), }; try { const savedDesign = await saveDesign(design, designId); setDesignData(savedDesign); if (onSave) { onSave(savedDesign); } alert('设计已保存!'); } catch (error) { console.error('Failed to save design:', error); alert('保存失败,请重试!'); } }; const handleAddShape = (shapeType: string) => { if (!canvasRef.current) return; const canvas = canvasRef.current; let shape; switch (shapeType) { case 'rectangle': shape = new fabric.Rect({ left: 100, top: 100, width: 100, height: 60, fill: '#ffffff', stroke: '#333333', strokeWidth: 2, }); break; case 'circle': shape = new fabric.Circle({ left: 100, top: 100, radius: 50, fill: '#ffffff', stroke: '#333333', strokeWidth: 2, }); break; case 'arrow': // 箭头实现 break; default: return; } canvas.add(shape); canvas.setActiveObject(shape); setSelectedObject(shape); canvas.renderAll(); }; return ( <div className="wpds-design-canvas"> {isLoading ? ( <div className="wpds-loading">加载中...</div> ) : ( <> <Toolbar selectedTool={selectedTool} onToolSelect={setSelectedTool} onSave={handleSave} /> <div className="wpds-canvas-container"> <ShapesLibrary onShapeAdd={handleAddShape} /> <FabricCanvas ref={canvasRef} selectedTool={selectedTool} onObjectSelect={setSelectedObject} /> <PropertiesPanel selectedObject={selectedObject} onPropertyChange={(properties) => { if (selectedObject && canvasRef.current) { selectedObject.set(properties); canvasRef.current.renderAll(); } }} /> </div> </> )} </div> ); }; 3.3 Fabric.js画布集成 // src/editor/components/FabricCanvas.tsx import React, { forwardRef, useImperativeHandle, useEffect } from 'react'; import { fabric } from 'fabric'; interface FabricCanvasProps { selectedTool: string; onObjectSelect: (object: any) => void; } export const FabricCanvas = forwardRef((props: FabricCanvasProps, ref) => { const canvasRef = React.useRef<HTMLCanvasElement>(null); const fabricCanvasRef = React.useRef<fabric.Canvas | null>(null); // 初始化画布 useEffect(() => { if (!canvasRef.current) return; const canvas = new fabric.Canvas(canvasRef.current, { width: 800, height: 600, backgroundColor: '#f5f5f5', selection: true, }); fabricCanvasRef.current = canvas; // 设置事件监听 canvas.on('selection:created', (e) => { props.onObjectSelect(e.selected[0]); }); canvas.on('selection:updated', (e) => { props.onObjectSelect(e.selected[0]); }); canvas.on('selection:cleared', () => { props.onObjectSelect(null); }); // 根据选择的工具设置画布模式 updateCanvasMode(props.selectedTool); return () => { canvas.dispose(); }; }, []); // 更新工具选择 useEffect(() => { updateCanvasMode(props.selectedTool); }, [props.selectedTool]); const updateCanvasMode = (tool: string) => { if (!fabricCanvasRef.current) return; const canvas = fabricCanvasRef.current; switch (tool) { case 'select': canvas.isDrawingMode = false; canvas.selection = true; canvas.defaultCursor = 'default'; break; case 'rectangle': canvas.isDrawingMode = false; canvas.selection = false; canvas.defaultCursor = 'crosshair'; setupRectangleDrawing(canvas); break; case 'line': canvas.isDrawingMode = true; canvas.freeDrawingBrush = new fabric.PencilBrush(canvas); canvas.freeDrawingBrush.width = 2; canvas.freeDrawingBrush.color = '#333333'; break; case 'text': canvas.isDrawingMode = false; canvas.selection = false; canvas.defaultCursor = 'text'; setupTextTool(canvas); break; } }; const setupRectangleDrawing = (canvas: fabric.Canvas) => { let rect: fabric.Rect | null = null; let isDrawing = false; let startX = 0; let startY = 0; canvas.on('mouse:down', (o) => { isDrawing = true; const pointer = canvas.getPointer(o.e); startX = pointer.x; startY = pointer.y; rect = new fabric.Rect({ left: startX, top: startY, width: 0, height: 0, fill: 'transparent', stroke: '#333333', strokeWidth: 2, }); canvas.add(rect); }); canvas.on('mouse:move', (o) => { if (!isDrawing || !rect) return; const pointer = canvas.getPointer(o.e); const width = pointer.x - startX; const height = pointer.y - startY; rect.set({ width: Math.abs(width), height: Math.abs(height), left: width > 0 ? startX : pointer.x, top: height > 0 ? startY : pointer.y, }); canvas.renderAll(); }); canvas.on('mouse:up', () => { isDrawing = false; if (rect && (rect.width === 0 || rect.height === 0)) { canvas.remove(rect); } rect = null; }); }; const setupTextTool = (canvas: fabric.Canvas) => { canvas.on('mouse:down', (o) => { const pointer = canvas.getPointer(o.e); const text = new fabric.IText('双击编辑文本', { left: pointer.x, top: pointer.y, fontSize: 16, fill: '#333333', }); canvas.add(text); canvas.setActiveObject(text); text.enterEditing(); text.selectAll(); }); }; // 暴露方法给父组件 useImperativeHandle(ref, () => ({ getCanvas: () => fabricCanvasRef.current, loadFromJSON: (json: any) => { if (fabricCanvasRef.current) { fabricCanvasRef.current.loadFromJSON(json, () => { fabricCanvasRef.current?.renderAll(); }); } }, toJSON: () => { return fabricCanvasRef.current?.toJSON(); }, })); return ( <div className="wpds-fabric-canvas"> <canvas ref={canvasRef} /> </div> ); }); 第四章:WordPress集成与Gutenberg块开发 4.1 创建Gutenberg块 为了让用户能在文章/页面中插入设计,我们需要创建Gutenberg块: // src/block/index.js import { registerBlockType } from '@wordpress/blocks'; import { Button, Modal } from '@wordpress/components'; import { useState } from '@wordpress/element'; import { DesignCanvas } from '../editor/components/DesignCanvas'; /design-block', { title: '设计图', icon: 'layout', category: 'embed', attributes: { designId: { type: 'number', default: 0 }, designTitle: { type: 'string', default: '' }, thumbnail: { type: 'string', default: '' }, width: { type: 'string', default: '100%' }, align: { type: 'string', default: 'center' } }, edit: function({ attributes, setAttributes }) { const [isModalOpen, setIsModalOpen] = useState(false); const [selectedDesign, setSelectedDesign] = useState(null); // 打开设计库 const openDesignLibrary = async () => { try { const response = await fetch('/wp-json/wpds/v1/designs'); const designs = await response.json(); // 这里应该显示设计库模态框 setIsModalOpen(true); } catch (error) { console.error('Failed to load designs:', error); } }; // 创建新设计 const createNewDesign = () => { setIsModalOpen(true); setSelectedDesign(null); }; // 选择设计 const handleSelectDesign = (design) => { setAttributes({ designId: design.id, designTitle: design.title, thumbnail: design.thumbnail }); setIsModalOpen(false); }; // 保存设计 const handleSaveDesign = (design) => { setAttributes({ designId: design.id, designTitle: design.title, thumbnail: design.thumbnail }); }; return ( <div className={`wpds-design-block align${attributes.align}`}> {attributes.designId ? ( <div className="wpds-design-preview"> <img src={attributes.thumbnail || `${wpds_plugin_url}/assets/images/default-thumbnail.png`} alt={attributes.designTitle} style={{ width: attributes.width }} /> <div className="wpds-design-actions"> <Button isSecondary onClick={() => setIsModalOpen(true)}> 编辑设计 </Button> <Button isDestructive onClick={() => { setAttributes({ designId: 0, designTitle: '', thumbnail: '' }); }}> 移除 </Button> </div> </div> ) : ( <div className="wpds-design-placeholder"> <p>插入设计图</p> <div className="wpds-design-buttons"> <Button isPrimary onClick={createNewDesign}> 创建新设计 </Button> <Button isSecondary onClick={openDesignLibrary}> 从库中选择 </Button> </div> </div> )} {isModalOpen && ( <Modal title={selectedDesign ? "编辑设计" : "创建新设计"} onRequestClose={() => setIsModalOpen(false)} className="wpds-design-modal" > <DesignCanvas designId={selectedDesign?.id} onSave={handleSaveDesign} /> </Modal> )} </div> ); }, save: function({ attributes }) { if (!attributes.designId) { return null; } return ( <div className={`wpds-design-embed align${attributes.align}`}> <div className="wpds-design-container" data-design-id={attributes.designId} style={{ maxWidth: attributes.width }} > {/* 这里将渲染实际的设计图 */} <div className="wpds-design-loading"> 加载设计中... </div> </div> </div> ); } }); ### 4.2 前端渲染设计图 当文章显示时,我们需要在前端渲染设计图: // src/frontend/render.jsdocument.addEventListener('DOMContentLoaded', function() { // 查找所有设计图容器 const designContainers = document.querySelectorAll('.wpds-design-container[data-design-id]'); designContainers.forEach(container => { const designId = container.getAttribute('data-design-id'); loadAndRenderDesign(designId, container); }); }); async function loadAndRenderDesign(designId, container) { try { // 获取设计数据 const response = await fetch(`/wp-json/wpds/v1/designs/${designId}`); const design = await response.json(); // 创建Canvas元素 const canvas = document.createElement('canvas'); canvas.width = 800; canvas.height = 600; canvas.className = 'wpds-rendered-canvas'; container.innerHTML = ''; container.appendChild(canvas); // 使用Fabric.js渲染设计 const fabricCanvas = new fabric.Canvas(canvas); fabricCanvas.loadFromJSON(JSON.parse(design.content), () => { fabricCanvas.renderAll(); // 添加交互功能 if (design.type === 'prototype') { addPrototypeInteractions(fabricCanvas, design); } }); } catch (error) { console.error('Failed to render design:', error); container.innerHTML = '<div class="wpds-design-error">无法加载设计图</div>'; } } function addPrototypeInteractions(canvas, design) { // 为UI原型添加点击交互 canvas.on('mouse:down', function(e) { if (e.target && e.target.linkTo) { // 处理原型链接 if (e.target.linkTo.startsWith('#')) { // 内部页面跳转 const targetScreen = design.screens.find(s => s.id === e.target.linkTo.substring(1)); if (targetScreen) { // 切换到目标屏幕 canvas.loadFromJSON(targetScreen.content, () => { canvas.renderAll(); }); } } else { // 外部链接 window.open(e.target.linkTo, '_blank'); } } }); // 添加悬停效果 canvas.on('mouse:over', function(e) { if (e.target && e.target.linkTo) { canvas.defaultCursor = 'pointer'; e.target.set('stroke', '#007cba'); canvas.renderAll(); } }); canvas.on('mouse:out', function(e) { if (e.target && e.target.linkTo) { canvas.defaultCursor = 'default'; e.target.set('stroke', e.target.originalStroke || '#333333'); canvas.renderAll(); } }); } ## 第五章:UI组件库与模板系统 ### 5.1 构建UI组件库 // src/editor/components/UIComponentsLibrary.tsximport React from 'react'; const UI_COMPONENTS = { basic: [ { id: 'button', name: '按钮', icon: 'button', component: ButtonComponent }, { id: 'input', name: '输入框', icon: 'input', component: InputComponent }, { id: 'textarea', name: '文本域', icon: 'textarea', component: TextareaComponent }, { id: 'dropdown', name: '下拉菜单', icon: 'dropdown', component: DropdownComponent }, ], layout: [ { id: 'header', name: '页眉', icon: 'header', component: HeaderComponent }, { id: 'footer', name: '页脚', icon: 'footer', component: FooterComponent }, { id: 'sidebar', name: '侧边栏', icon: 'sidebar', component: SidebarComponent }, { id: 'navbar', name: '导航栏', icon: 'navbar', component: NavbarComponent }, ], mobile: [ { id: 'mobile-header', name: '移动端页眉', icon: 'smartphone', component: MobileHeaderComponent }, { id: 'tab-bar', name: '标签栏', icon: 'tab-bar', component: TabBarComponent }, { id: 'list-item', name: '列表项', icon: 'list', component: ListItemComponent }, ] }; export const UIComponentsLibrary: React.FC<{ onComponentAdd: (component: any) => void }> = ({ onComponentAdd }) => { const [activeCategory, setActiveCategory] = React.useState('basic'); const handleDragStart = (e: React.DragEvent, component: any) => { e.dataTransfer.setData('application/wpds-component', JSON.stringify(component)); }; return ( <div className="wpds-ui-library"> <div className="wpds-ui-categories"> {Object.keys(UI_COMPONENTS).map(category => ( <button key={category} className={`wpds-category-btn ${activeCategory === category ? 'active' : ''}`} onClick={() => setActiveCategory(category)} > {category === 'basic' ? '基础' : category === 'layout' ? '布局' : '移动端'} </button> ))} </div> <div className="wpds-components-grid"> {UI_COMPONENTS[activeCategory].map(component => ( <div key={component.id} className="wpds-component-item" draggable onDragStart={(e) => handleDragStart(e, component)} onClick={() => onComponentAdd(component)} > <div className="wpds-component-icon"> {/* 这里放置图标 */} </div> <span className="wpds-component-name">{component.name}</span> </div> ))} </div> </div> ); }; // 按钮组件定义const ButtonComponent = { type: 'button', config: { text: '按钮', width: 100, height: 40, backgroundColor: '#007cba', color: '#ffffff', borderRadius: 4, fontSize: 14, }, create: function(config = {}) { return new fabric.Rect({ width: config.width || 100, height: config.height || 40, fill: config.backgroundColor || '#007cba', rx: config.borderRadius || 4, ry: config.borderRadius || 4, strokeWidth: 0, data: { type: 'button', config: config } }); } }; ### 5.2 模板系统实现 // includes/class-templates.phpclass WPDS_Templates { private static $instance = null; private $templates = []; public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } private function __construct() { $this->load_default_templates(); add_action('init', [$this, 'register_template_post_type']); } public function register_template_post_type() { register_post_type('wpds_template', [ 'labels' => [ 'name' => __('设计模板', 'wpds'), 'singular_name' => __('模板', 'wpds'), ], 'public' => false, 'show_ui' => true, 'show_in_menu' => 'wpds-design-studio', 'supports' => ['title', 'thumbnail'], 'capability_type' => 'post', ]); } private function load_default_templates() { $this->templates = [ 'flowchart-basic' => [ 'name' => '基础流程图', 'type' => 'flowchart', 'thumbnail' => WPDS_PLUGIN_URL . 'assets/templates/flowchart-basic.png', 'content' => json_encode([ 'objects' => [ // 默认的流程图元素 ], 'background' => '#ffffff' ]) ], 'website-wireframe' => [ 'name' => '网站线框图', 'type' => 'wireframe', 'thumbnail' => WPDS_PLUGIN_URL . 'assets/templates/website-wireframe.png', 'content' => json_encode([ 'objects' => [ // 网站线框图元素 ], 'background' => '#f5f5f5' ]) ], 'mobile-app' => [ 'name' => '移动应用原型', 'type' => 'prototype', 'thumbnail' => WPDS_PLUGIN_URL . 'assets/templates/mobile-app.png', 'content' => json_encode([ 'screens' => [ // 多个屏幕定义 ] ]) ] ]; } public function get_templates($type = '') { if ($type) { return array_filter($this->templates, function($template) use ($type) { return $template['type'] === $type; }); } return $this->templates; } public function apply_template($template_id, $canvas) { if (!isset($this->templates[$template_id])) { return false; } $template = $this->templates[$template_id]; $canvas.loadFromJSON(json_decode($template['content'], true)); return true; } } ## 第六章:高级功能实现 ### 6.1 实时协作功能 // src/collaboration/CollaborationManager.jsimport Pusher from 'pusher-js'; export class CollaborationManager { constructor(designId, userId) { this.designId = designId; this.userId = userId; this.canvas = null; this.pusher = null; this.channel = null; this.cursors = new Map(); this.initializePusher(); } initializePusher() { // 初始化Pusher连接 this.pusher = new Pusher(wpds_pusher_key, { cluster: wpds_pusher_cluster, authEndpoint: '/wp-json/wpds/v1/pusher/auth', auth: { params: { user_id: this.userId, design_id: this.designId } } }); this.channel = this.pusher.subscribe(`private-design-${this.designId}`); // 监听其他用户的操作 this.channel.bind('client-object-added', this.handleObjectAdded.bind(this)); this.channel.bind('client-object-modified', this.handleObjectModified.bind(this)); this.channel.bind('client-object-removed', this.handleObjectRemoved.bind(this)); this.channel.bind('client-cursor-moved', this.handleCursorMoved.bind(this)); } setCanvas(canvas) { this.canvas = canvas; this.setupCanvasEvents(); } setupCanvasEvents() { if (!this.canvas) return; // 监听画布变化并广播 this.canvas.on('object:added', (e) => { if (e.target.__local) return; // 避免循环 this.broadcast('object-added', e.target.toJSON()); }); this.canvas.on('object:modified', (e) => { if (e.target.__local) return; this.broadcast('object-modified', { id: e.target.id, properties: e.target.toJSON() }); }); this.canvas.on('object:removed', (e) => { if (e.target.__local) return; this.broadcast('object-removed', e.target.id); }); // 监听鼠标移动 this.canvas.on('mouse:move', (e) => { const pointer = this.canvas.getPointer(e.e); this.broadcast('cursor-moved', { x: pointer.x, y: pointer.y, userId: this.userId }); }); } broadcast(event, data) { if (!this.channel) return; data.timestamp = Date.now(); data.userId = this.userId; this.channel.trigger(`client-${event}`, data); } handleObjectAdded(data) { if (data.userId === this.userId) return; fabric.util.enlivenObjects([data], (objects) => { objects.forEach(obj => { obj.__remote = true; this.canvas.add(obj); }); this.canvas.renderAll(); }); } handleObjectModified(data) { if (data.userId === this.userId) return; const obj = this.canvas.getObjects().find(o => o.id === data.id); if (obj) { obj.__remote = true; obj.set(data.properties); this.canvas.renderAll(); } } handleCursorMoved(data) { if (data.userId === this.userId) return; this.updateUserCursor(data.userId, data.x, data.y); } updateUserCursor(userId, x, y) { let cursor = this.cursors.get(userId); if (!cursor) { // 创建新的光标 cursor = new fabric.Circle({ radius: 5, fill: this.getUserColor(userId), left: x, top: y, selectable: false, hasControls: false, hasBorders: false }); // 添加用户标签 const text = new fabric.Text(`用户${userId}`, { left: x + 10, top: y - 10, fontSize: 12, fill: this.getUserColor(userId) }); cursor.label = text; this.canvas.add(cursor); this.canvas.add(text); this.cursors.set(userId, { cursor, label: text }); } else { // 更新现有光标位置 cursor.cursor.set({ left: x, top: y }); cursor.label.set({ left: x + 10, top: y - 10 }); } this.canvas.renderAll(); } getUserColor(userId) { const colors = ['#FF6B6B', '#4ECDC4', '#FFD166', '#06D6A0', '#118AB2']; return colors[userId % colors.length]; } destroy() { if (this.pusher) { this.pusher.disconnect(); } } }
发表评论