跳至内容

分类: 应用软件

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

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

发表评论

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

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

发表评论

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

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

发表评论

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

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

发表评论

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

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

发表评论

WordPress插件开发教程,实现网站内容自动生成语音并转换为播客

WordPress插件开发教程:实现网站内容自动生成语音并转换为播客 引言:当文字遇见声音 在信息爆炸的数字时代,内容呈现方式的多样性变得至关重要。据统计,全球播客听众数量已从2019年的3.32亿增长到2023年的4.64亿,预计到2024年将超过5亿。与此同时,语音技术的发展使得文字转语音的质量达到了前所未有的自然度。对于WordPress网站运营者而言,将文字内容自动转换为语音并生成播客,不仅能提升用户体验,还能扩大内容传播渠道,吸引更多受众。 本教程将深入讲解如何通过WordPress插件开发,实现网站内容自动生成语音并转换为播客的功能。我们将从基础概念讲起,逐步深入到代码实现,最终打造一个功能完整的插件。无论您是WordPress开发者还是有一定技术基础的内容创作者,都能通过本教程掌握这一实用技能。 第一部分:准备工作与环境搭建 1.1 理解WordPress插件架构 WordPress插件系统基于PHP构建,采用事件驱动的钩子(Hooks)机制。插件通过动作钩子(Actions)和过滤器钩子(Filters)与WordPress核心交互。理解这一机制是开发任何WordPress插件的基础。 一个标准的WordPress插件通常包含以下结构: 主插件文件(plugin-name.php):包含插件元信息 功能类文件:实现核心功能 资源文件:CSS、JavaScript、图像等 语言文件:用于国际化 1.2 开发环境配置 在开始开发前,需要搭建合适的开发环境: 本地开发环境:推荐使用XAMPP、MAMP或Local by Flywheel 代码编辑器:VS Code、PHPStorm或Sublime Text WordPress安装:最新版本的WordPress 调试工具:安装Query Monitor、Debug Bar等调试插件 版本控制:使用Git进行代码管理 1.3 创建插件基础结构 首先,在WordPress的wp-content/plugins/目录下创建新文件夹auto-podcast-generator,然后创建主插件文件: <?php /** * Plugin Name: Auto Podcast Generator * Plugin URI: https://yourwebsite.com/auto-podcast-generator * Description: 自动将WordPress文章转换为语音播客 * Version: 1.0.0 * Author: Your Name * License: GPL v2 or later * Text Domain: auto-podcast-generator */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('APG_VERSION', '1.0.0'); define('APG_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('APG_PLUGIN_URL', plugin_dir_url(__FILE__)); define('APG_PLUGIN_BASENAME', plugin_basename(__FILE__)); // 初始化插件 require_once APG_PLUGIN_DIR . 'includes/class-auto-podcast-generator.php'; function run_auto_podcast_generator() { $plugin = new Auto_Podcast_Generator(); $plugin->run(); } run_auto_podcast_generator(); 第二部分:核心功能设计与实现 2.1 语音生成引擎选择与集成 目前市场上有多种文字转语音(TTS)服务可供选择: Google Cloud Text-to-Speech:质量高,支持多种语言和声音 Amazon Polly:提供逼真的语音合成 Microsoft Azure Cognitive Services:语音自然度高 IBM Watson Text to Speech:企业级解决方案 开源方案:如eSpeak、Festival(质量较低) 本教程将使用Google Cloud Text-to-Speech API作为示例,因为它提供了高质量的语音合成和相对友好的免费额度。 2.1.1 配置Google Cloud TTS API 首先,需要在Google Cloud Console中创建项目并启用Text-to-Speech API,然后创建服务账号密钥。 在插件中创建API配置类: <?php // includes/class-tts-engine.php class APG_TTS_Engine { private $api_key; private $language_code; private $voice_name; public function __construct() { $options = get_option('apg_settings'); $this->api_key = isset($options['google_tts_api_key']) ? $options['google_tts_api_key'] : ''; $this->language_code = isset($options['tts_language']) ? $options['tts_language'] : 'en-US'; $this->voice_name = isset($options['tts_voice']) ? $options['tts_voice'] : 'en-US-Wavenet-D'; } /** * 将文本转换为语音 * * @param string $text 要转换的文本 * @param int $post_id 文章ID * @return string|bool 返回音频文件路径或false */ public function text_to_speech($text, $post_id) { if (empty($this->api_key)) { error_log('Auto Podcast Generator: Google TTS API key not configured'); return false; } // 清理文本,移除HTML标签 $clean_text = wp_strip_all_tags($text); // 限制文本长度(Google TTS API限制为5000字符) if (strlen($clean_text) > 5000) { $clean_text = $this->truncate_text($clean_text, 5000); } // 准备API请求 $url = 'https://texttospeech.googleapis.com/v1/text:synthesize?key=' . $this->api_key; $data = array( 'input' => array( 'text' => $clean_text ), 'voice' => array( 'languageCode' => $this->language_code, 'name' => $this->voice_name ), 'audioConfig' => array( 'audioEncoding' => 'MP3', 'speakingRate' => 1.0, 'pitch' => 0, 'volumeGainDb' => 0 ) ); $args = array( 'body' => json_encode($data), 'headers' => array( 'Content-Type' => 'application/json' ), 'timeout' => 30 ); // 发送请求 $response = wp_remote_post($url, $args); if (is_wp_error($response)) { error_log('Auto Podcast Generator: TTS API request failed - ' . $response->get_error_message()); return false; } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if (isset($data['error'])) { error_log('Auto Podcast Generator: TTS API error - ' . $data['error']['message']); return false; } if (!isset($data['audioContent'])) { error_log('Auto Podcast Generator: No audio content in response'); return false; } // 解码base64音频数据 $audio_data = base64_decode($data['audioContent']); // 保存音频文件 $upload_dir = wp_upload_dir(); $podcast_dir = $upload_dir['basedir'] . '/apg-podcasts/'; if (!file_exists($podcast_dir)) { wp_mkdir_p($podcast_dir); } $filename = 'podcast-' . $post_id . '-' . time() . '.mp3'; $filepath = $podcast_dir . $filename; if (file_put_contents($filepath, $audio_data)) { // 保存文件信息到文章元数据 $file_url = $upload_dir['baseurl'] . '/apg-podcasts/' . $filename; update_post_meta($post_id, '_apg_audio_file', $filepath); update_post_meta($post_id, '_apg_audio_url', $file_url); update_post_meta($post_id, '_apg_audio_generated', current_time('mysql')); return $filepath; } return false; } /** * 截断文本,尽量在句子结束处截断 */ private function truncate_text($text, $max_length) { if (strlen($text) <= $max_length) { return $text; } $truncated = substr($text, 0, $max_length); $last_period = strrpos($truncated, '.'); $last_question = strrpos($truncated, '?'); $last_exclamation = strrpos($truncated, '!'); $last_sentence_end = max($last_period, $last_question, $last_exclamation); if ($last_sentence_end > 0) { return substr($text, 0, $last_sentence_end + 1); } return $truncated . '...'; } /** * 获取可用的语音列表 */ public function get_available_voices() { // 这里可以缓存语音列表以提高性能 $voices = get_transient('apg_google_voices'); if (false === $voices) { $url = 'https://texttospeech.googleapis.com/v1/voices?key=' . $this->api_key; $response = wp_remote_get($url); if (!is_wp_error($response)) { $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if (isset($data['voices'])) { $voices = $data['voices']; set_transient('apg_google_voices', $voices, WEEK_IN_SECONDS); } } } return $voices; } } 2.2 文章内容处理与优化 直接使用文章原始内容生成语音可能不是最佳体验,我们需要对内容进行处理: <?php // includes/class-content-processor.php class APG_Content_Processor { /** * 处理文章内容,准备用于语音合成 */ public function prepare_content_for_tts($post_id) { $post = get_post($post_id); if (!$post || $post->post_status !== 'publish') { return false; } $content = $post->post_content; // 应用the_content过滤器,处理短代码等 $content = apply_filters('the_content', $content); // 移除不需要的元素 $content = $this->clean_content($content); // 添加文章标题作为介绍 $title = get_the_title($post_id); $final_content = "文章标题: " . $title . ". " . $content; // 添加结尾语 $final_content .= " 本文由自动播客生成器为您朗读。访问我们的网站获取更多内容。"; return $final_content; } /** * 清理HTML内容,提取纯文本 */ private function clean_content($content) { // 移除脚本和样式标签 $content = preg_replace('/<scriptb[^>]*>(.*?)</script>/is', '', $content); $content = preg_replace('/<styleb[^>]*>(.*?)</style>/is', '', $content); // 移除注释 $content = preg_replace('/<!--(.*?)-->/', '', $content); // 替换HTML标签为适当的停顿 $content = preg_replace('/</h[1-6]>/', '. ', $content); $content = preg_replace('/</p>/', '. ', $content); $content = preg_replace('/</div>/', '. ', $content); $content = preg_replace('/<brs*/?>/', '. ', $content); // 移除所有剩余HTML标签 $content = wp_strip_all_tags($content); // 规范化空格和标点 $content = preg_replace('/s+/', ' ', $content); $content = preg_replace('/s*.s*/', '. ', $content); $content = preg_replace('/s*,s*/', ', ', $content); $content = preg_replace('/s*!s*/', '! ', $content); $content = preg_replace('/s*?s*/', '? ', $content); // 解码HTML实体 $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5, 'UTF-8'); return trim($content); } /** * 估算朗读时间 */ public function estimate_reading_time($content) { // 平均阅读速度:每分钟150-200字 $word_count = str_word_count(strip_tags($content)); $minutes = ceil($word_count / 180); // 使用180字/分钟作为平均值 return $minutes; } } 2.3 播客RSS Feed生成 播客的核心是RSS Feed,它使音频内容能够被播客客户端订阅: <?php // includes/class-podcast-feed.php class APG_Podcast_Feed { private $feed_slug = 'podcast-feed'; public function __construct() { add_action('init', array($this, 'add_podcast_feed')); add_action('do_feed_' . $this->feed_slug, array($this, 'generate_podcast_feed'), 10, 1); } /** * 注册自定义RSS Feed */ public function add_podcast_feed() { add_feed($this->feed_slug, array($this, 'generate_podcast_feed')); } /** * 生成播客RSS Feed */ public function generate_podcast_feed() { // 设置内容类型为XML header('Content-Type: ' . feed_content_type('rss2') . '; charset=' . get_option('blog_charset'), true); // 获取播客设置 $options = get_option('apg_settings'); // 查询有音频的文章 $args = array( 'post_type' => 'post', 'post_status' => 'publish', 'meta_key' => '_apg_audio_url', 'meta_compare' => 'EXISTS', 'posts_per_page' => 20, 'orderby' => 'date', 'order' => 'DESC' ); $podcast_posts = new WP_Query($args); echo '<?xml version="1.0" encoding="' . get_option('blog_charset') . '"?>'; ?> <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"> <channel> <title><?php echo esc_xml(get_bloginfo('name') . ' Podcast'); ?></title> <atom:link href="<?php self_link(); ?>" rel="self" type="application/rss+xml" /> <link><?php echo esc_url(home_url('/')); ?></link> <description><?php echo esc_xml(get_bloginfo('description')); ?></description> <lastBuildDate><?php echo mysql2date('D, d M Y H:i:s +0000', get_lastpostmodified('GMT'), false); ?></lastBuildDate> <language><?php echo get_option('rss_language'); ?></language> <sy:updatePeriod><?php echo apply_filters('rss_update_period', 'hourly'); ?></sy:updatePeriod> <sy:updateFrequency><?php echo apply_filters('rss_update_frequency', '1'); ?></sy:updateFrequency> <generator>Auto Podcast Generator for WordPress</generator> <!-- iTunes播客特定标签 --> <itunes:subtitle><?php echo esc_xml($options['podcast_subtitle'] ?? get_bloginfo('description')); ?></itunes:subtitle> <itunes:summary><?php echo esc_xml($options['podcast_summary'] ?? get_bloginfo('description')); ?></itunes:summary> <itunes:author><?php echo esc_xml($options['podcast_author'] ?? get_bloginfo('name')); ?></itunes:author> <?php if (!empty($options['podcast_image'])): ?> <itunes:image href="<?php echo esc_url($options['podcast_image']); ?>" /> <image> <url><?php echo esc_url($options['podcast_image']); ?></url> <title><?php echo esc_xml(get_bloginfo('name') . ' Podcast'); ?></title> <link><?php echo esc_url(home_url('/')); ?></link> </image> <?php endif; ?> <itunes:explicit><?php echo (!empty($options['podcast_explicit']) && $options['podcast_explicit'] === 'yes') ? 'yes' : 'no'; ?></itunes:explicit> <itunes:category text="<?php echo esc_attr($options['podcast_category'] ?? 'Technology'); ?>"> <?php if (!empty($options['podcast_subcategory'])): ?> <itunes:category text="<?php echo esc_attr($options['podcast_subcategory']); ?>" /> <?php endif; ?> </itunes:category> ): ?> <?php while ($podcast_posts->have_posts()): $podcast_posts->the_post(); ?> <?php $audio_url = get_post_meta(get_the_ID(), '_apg_audio_url', true); $audio_file = get_post_meta(get_the_ID(), '_apg_audio_file', true); $audio_duration = get_post_meta(get_the_ID(), '_apg_audio_duration', true); if (empty($audio_url)) { continue; } // 获取音频文件大小 $file_size = 0; if (file_exists($audio_file)) { $file_size = filesize($audio_file); } // 获取文章特色图像 $thumbnail_url = get_the_post_thumbnail_url(get_the_ID(), 'full'); ?> <item> <title><?php echo esc_xml(get_the_title()); ?></title> <link><?php echo esc_url(get_permalink()); ?></link> <pubDate><?php echo mysql2date('D, d M Y H:i:s +0000', get_post_time('Y-m-d H:i:s', true), false); ?></pubDate> <dc:creator><![CDATA[<?php the_author(); ?>]]></dc:creator> <guid isPermaLink="false"><?php the_guid(); ?></guid> <description><![CDATA[<?php echo esc_xml(get_the_excerpt()); ?>]]></description> <content:encoded><![CDATA[<?php echo esc_xml(get_the_content()); ?>]]></content:encoded> <!-- 播客特定元素 --> <enclosure url="<?php echo esc_url($audio_url); ?>" length="<?php echo esc_attr($file_size); ?>" type="audio/mpeg" /> <itunes:author><?php the_author(); ?></itunes:author> <itunes:subtitle><?php echo esc_xml(wp_trim_words(get_the_excerpt(), 20)); ?></itunes:subtitle> <itunes:summary><![CDATA[<?php echo esc_xml(get_the_excerpt()); ?>]]></itunes:summary> <?php if (!empty($thumbnail_url)): ?> <itunes:image href="<?php echo esc_url($thumbnail_url); ?>" /> <?php endif; ?> <?php if (!empty($audio_duration)): ?> <itunes:duration><?php echo esc_attr($audio_duration); ?></itunes:duration> <?php endif; ?> <itunes:explicit><?php echo (!empty($options['podcast_explicit']) && $options['podcast_explicit'] === 'yes') ? 'yes' : 'no'; ?></itunes:explicit> </item> <?php endwhile; ?> <?php wp_reset_postdata(); ?> <?php endif; ?> </channel> </rss> <?php } /** * 获取播客Feed URL */ public function get_feed_url() { return home_url('/feed/' . $this->feed_slug); } } ## 第三部分:插件核心类与集成 ### 3.1 主插件类实现 <?php// includes/class-auto-podcast-generator.php class Auto_Podcast_Generator { private $loader; private $tts_engine; private $content_processor; private $podcast_feed; private $admin; public function __construct() { $this->load_dependencies(); $this->define_admin_hooks(); $this->define_public_hooks(); } private function load_dependencies() { require_once APG_PLUGIN_DIR . 'includes/class-tts-engine.php'; require_once APG_PLUGIN_DIR . 'includes/class-content-processor.php'; require_once APG_PLUGIN_DIR . 'includes/class-podcast-feed.php'; require_once APG_PLUGIN_DIR . 'admin/class-admin.php'; $this->tts_engine = new APG_TTS_Engine(); $this->content_processor = new APG_Content_Processor(); $this->podcast_feed = new APG_Podcast_Feed(); $this->admin = new APG_Admin(); } private function define_admin_hooks() { add_action('admin_menu', array($this->admin, 'add_admin_menu')); add_action('admin_init', array($this->admin, 'register_settings')); add_action('add_meta_boxes', array($this, 'add_podcast_meta_box')); add_action('save_post', array($this, 'save_post_handler'), 10, 2); add_action('admin_enqueue_scripts', array($this->admin, 'enqueue_admin_scripts')); } private function define_public_hooks() { // 在文章内容后添加音频播放器 add_filter('the_content', array($this, 'add_audio_player_to_content')); // 添加播客订阅链接到页面 add_action('wp_footer', array($this, 'add_podcast_subscription_links')); // 短代码支持 add_shortcode('apg_podcast_player', array($this, 'podcast_player_shortcode')); } /** * 添加播客元数据框 */ public function add_podcast_meta_box() { $post_types = apply_filters('apg_supported_post_types', array('post')); foreach ($post_types as $post_type) { add_meta_box( 'apg_podcast_meta', __('播客设置', 'auto-podcast-generator'), array($this, 'render_podcast_meta_box'), $post_type, 'side', 'default' ); } } /** * 渲染播客元数据框 */ public function render_podcast_meta_box($post) { wp_nonce_field('apg_podcast_meta', 'apg_podcast_meta_nonce'); $audio_url = get_post_meta($post->ID, '_apg_audio_url', true); $audio_generated = get_post_meta($post->ID, '_apg_audio_generated', true); $generate_audio = get_post_meta($post->ID, '_apg_generate_audio', true); if (empty($generate_audio)) { $generate_audio = 'auto'; } ?> <div class="apg-meta-box"> <p> <label for="apg_generate_audio"> <strong><?php _e('生成音频', 'auto-podcast-generator'); ?></strong> </label> </p> <p> <select name="apg_generate_audio" id="apg_generate_audio" style="width:100%;"> <option value="auto" <?php selected($generate_audio, 'auto'); ?>> <?php _e('自动(发布时生成)', 'auto-podcast-generator'); ?> </option> <option value="manual" <?php selected($generate_audio, 'manual'); ?>> <?php _e('手动生成', 'auto-podcast-generator'); ?> </option> <option value="disabled" <?php selected($generate_audio, 'disabled'); ?>> <?php _e('不生成', 'auto-podcast-generator'); ?> </option> </select> </p> <?php if (!empty($audio_url)): ?> <p> <strong><?php _e('音频状态:', 'auto-podcast-generator'); ?></strong><br> <?php _e('已生成', 'auto-podcast-generator'); ?> <?php if ($audio_generated): ?> <br><small><?php echo date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($audio_generated)); ?></small> <?php endif; ?> </p> <p> <audio controls style="width:100%;"> <source src="<?php echo esc_url($audio_url); ?>" type="audio/mpeg"> <?php _e('您的浏览器不支持音频播放。', 'auto-podcast-generator'); ?> </audio> </p> <p> <a href="<?php echo esc_url($audio_url); ?>" target="_blank" class="button button-small"> <?php _e('下载音频', 'auto-podcast-generator'); ?> </a> <button type="button" class="button button-small button-secondary" id="apg_regenerate_audio"> <?php _e('重新生成', 'auto-podcast-generator'); ?> </button> </p> <?php else: ?> <p> <strong><?php _e('音频状态:', 'auto-podcast-generator'); ?></strong><br> <?php _e('未生成', 'auto-podcast-generator'); ?> </p> <?php endif; ?> <div id="apg_audio_generation_message" style="display:none; margin-top:10px; padding:5px; background:#f5f5f5; border-left:4px solid #46b450;"></div> </div> <script> jQuery(document).ready(function($) { $('#apg_regenerate_audio').on('click', function(e) { e.preventDefault(); var button = $(this); var messageDiv = $('#apg_audio_generation_message'); button.prop('disabled', true).text('<?php _e("生成中...", "auto-podcast-generator"); ?>'); messageDiv.hide().removeClass('notice-success notice-error'); $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'apg_generate_audio', post_id: <?php echo $post->ID; ?>, nonce: '<?php echo wp_create_nonce("apg_generate_audio_" . $post->ID); ?>' }, success: function(response) { if (response.success) { messageDiv.addClass('notice-success').html('<p><?php _e("音频生成成功!页面将重新加载...", "auto-podcast-generator"); ?></p>').show(); setTimeout(function() { location.reload(); }, 2000); } else { messageDiv.addClass('notice-error').html('<p>' + response.data + '</p>').show(); button.prop('disabled', false).text('<?php _e("重新生成", "auto-podcast-generator"); ?>'); } }, error: function() { messageDiv.addClass('notice-error').html('<p><?php _e("生成失败,请重试。", "auto-podcast-generator"); ?></p>').show(); button.prop('disabled', false).text('<?php _e("重新生成", "auto-podcast-generator"); ?>'); } }); }); }); </script> <?php } /** * 保存文章时的处理 */ public function save_post_handler($post_id, $post) { // 检查权限 if (!current_user_can('edit_post', $post_id)) { return; } // 验证nonce if (!isset($_POST['apg_podcast_meta_nonce']) || !wp_verify_nonce($_POST['apg_podcast_meta_nonce'], 'apg_podcast_meta')) { return; } // 防止自动保存时处理 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { return; } // 保存生成选项 if (isset($_POST['apg_generate_audio'])) { update_post_meta($post_id, '_apg_generate_audio', sanitize_text_field($_POST['apg_generate_audio'])); } // 如果是发布状态且设置为自动生成,则生成音频 if ($post->post_status === 'publish' && isset($_POST['apg_generate_audio']) && $_POST['apg_generate_audio'] === 'auto') { $this->generate_audio_for_post($post_id); } } /** * 为文章生成音频 */ public function generate_audio_for_post($post_id) { // 检查是否已生成 $existing_audio = get_post_meta($post_id, '_apg_audio_url', true); if (!empty($existing_audio)) { return true; } // 准备内容 $content = $this->content_processor->prepare_content_for_tts($post_id); if (empty($content)) { return false; } // 生成音频 $audio_file = $this->tts_engine->text_to_speech($content, $post_id); if ($audio_file) { // 计算音频时长 $this->calculate_audio_duration($post_id, $audio_file); // 触发动作,允许其他插件响应 do_action('apg_audio_generated', $post_id, $audio_file); return true; } return false; } /** * 计算音频时长 */ private function calculate_audio_duration($post_id, $audio_file) { if (!file_exists($audio_file)) { return; } // 使用getID3库获取音频信息 if (!class_exists('getID3')) { require_once(ABSPATH . 'wp-admin/includes/media.php'); } try { $id3 = new getID3(); $file_info = $id3->analyze($audio_file); if (isset($file_info['playtime_seconds'])) { $duration_seconds = floor($file_info['playtime_seconds']); $duration_formatted = sprintf('%02d:%02d:%02d', floor($duration_seconds / 3600), floor(($duration_seconds % 3600) / 60), $duration_seconds % 60 ); update_post_meta($post_id, '_apg_audio_duration', $duration_formatted); update_post_meta($post_id, '_apg_audio_duration_seconds', $duration_seconds); } } catch (Exception $e) { error_log('Auto Podcast Generator: Could not calculate audio duration - ' . $e->getMessage()); } } /** * 在文章内容后添加音频播放器 */ public function add_audio_player_to_content($content) { if (!is_single() || !in_the_loop() || !is_main_query()) { return $content; } $post_id = get_the_ID(); $audio_url = get_post_meta($post_id, '_apg_audio_url', true); if (empty($audio_url)) { return $content; } $options = get_option('apg_settings'); $show_player = isset($options['show_player_in_content']) ? $options['show_player_in_content'] : 'yes'; if ($show_player !== 'yes') { return $content; } $player_position = isset($options['player_position']) ? $options['player_position'] : 'after'; $player_html = $this->get_audio_player_html($post_id); if ($player_position === 'before') { return $player_html . $content; } else { return $content . $player_html; } } /** * 获取音频播放器HTML */ private function get_audio_player_html($post_id) { $audio_url = get_post_meta($post_id, '_apg_audio_url', true); $audio_duration = get_post_meta($post_id, '_apg_audio_duration', true); ob_start(); ?> <div class="apg-audio-player" style="margin: 30px 0; padding: 20px; background: #f9f9f9; border-radius: 8px; border-left: 4px solid #0073aa;"> <h3 style="margin-top: 0; color: #333;"> <?php _e('收听本文音频版', 'auto-podcast-generator'); ?> <?php if ($audio_duration): ?> <small style="font-size: 14px; color: #666; font-weight: normal;"> (<?php echo esc_html($audio_duration); ?>) </small> <?php endif; ?> </h3> <audio controls style="width:100%;"> <source src="<?php echo esc_url($audio_url); ?>" type="audio/mpeg"> <?php _e('您的浏览器不支持音频播放。', 'auto-podcast-generator'); ?> </audio> <div style="margin-top: 15px; font-size: 14px; color: #666;"> <p style="margin: 5px 0;"> <?php _e('您也可以:', 'auto-podcast-generator'); ?> <a href="<?php echo esc_url($audio_url); ?>" download style="margin-left: 10px;"> <?php _e('下载音频', 'auto-podcast-generator'); ?> </a> <a href="<?php echo esc_url($this->podcast_feed->get_feed_url()); ?>" style="margin-left: 10px;" target="_blank"> <?php _e('订阅播客', 'auto-podcast-generator'); ?> </a> </p> </div> </div> <?php return ob_get_clean(); } /** * 添加播客订阅链接 */ public function add_podcast_subscription_links() { $options = get_option('apg_settings'); $show_subscription_links = isset($options['show_subscription_links']) ? $options['show_subscription_links'] : 'yes'; if ($show

发表评论

实战教程,在网站中添加在线个人财务记账与预算管理小程序

实战教程:在WordPress网站中添加在线个人财务记账与预算管理小程序 引言:为什么网站需要个人财务工具? 在当今数字化时代,个人财务管理已成为许多人日常生活中的重要组成部分。随着在线消费、数字支付和远程工作的普及,人们越来越需要便捷的工具来跟踪和管理个人财务。对于网站运营者而言,在网站上集成实用的个人财务工具不仅能增加用户粘性,还能提升网站的价值和实用性。 WordPress作为全球最流行的内容管理系统,其强大的可扩展性使其成为实现此类功能的理想平台。通过代码二次开发,我们可以在WordPress网站上添加一个完整的在线个人财务记账与预算管理小程序,为用户提供实用的互联网小工具。 本教程将详细指导您如何通过WordPress代码二次开发,实现一个功能完整的个人财务管理系统,涵盖记账、预算管理、数据可视化等核心功能。 第一部分:项目规划与环境准备 1.1 功能需求分析 在开始开发之前,我们需要明确个人财务管理系统应包含的核心功能: 用户认证与数据隔离:确保每个用户只能访问自己的财务数据 收入与支出记录:支持添加、编辑、删除和分类财务记录 预算管理:允许用户设置月度/年度预算并跟踪执行情况 数据可视化:通过图表展示财务趋势和分类占比 数据导出:支持将财务数据导出为CSV或Excel格式 报表生成:自动生成月度/年度财务报告 1.2 开发环境搭建 为了进行WordPress二次开发,我们需要准备以下环境: 本地开发环境:安装XAMPP、MAMP或Local by Flywheel WordPress安装:最新版本的WordPress核心文件 代码编辑器:VS Code、Sublime Text或PHPStorm 浏览器开发者工具:用于调试前端代码 1.3 创建自定义插件 我们将通过创建自定义插件的方式实现财务管理系统,这样可以确保代码的独立性和可维护性。 在WordPress的wp-content/plugins目录下创建新文件夹personal-finance-manager,并在其中创建主插件文件: <?php /** * Plugin Name: 个人财务记账与预算管理系统 * Plugin URI: https://yourwebsite.com/ * Description: 在WordPress网站中添加在线个人财务记账与预算管理功能 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('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-pfm-init.php'; 第二部分:数据库设计与数据模型 2.1 创建自定义数据库表 个人财务数据需要专门的数据库表来存储。我们将在插件激活时创建这些表: // 在includes/class-pfm-db.php中 class PFM_DB { public static function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); // 财务记录表 $table_name = $wpdb->prefix . 'pfm_transactions'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, type varchar(20) NOT NULL COMMENT 'income/expense', category varchar(100) NOT NULL, amount decimal(10,2) NOT NULL, description text, transaction_date date NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY user_id (user_id), KEY transaction_date (transaction_date), KEY category (category) ) $charset_collate;"; // 预算表 $budget_table = $wpdb->prefix . 'pfm_budgets'; $budget_sql = "CREATE TABLE IF NOT EXISTS $budget_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, category varchar(100) NOT NULL, budget_amount decimal(10,2) NOT NULL, period varchar(20) NOT NULL COMMENT 'monthly/yearly', start_date date NOT NULL, end_date date, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY user_id (user_id), KEY period (period) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); dbDelta($budget_sql); } } 2.2 数据模型类 创建数据模型类来处理财务数据的CRUD操作: // 在includes/class-pfm-transaction.php中 class PFM_Transaction { private $db; private $table_name; public function __construct() { global $wpdb; $this->db = $wpdb; $this->table_name = $wpdb->prefix . 'pfm_transactions'; } // 添加财务记录 public function add($user_id, $data) { $defaults = array( 'type' => 'expense', 'category' => '其他', 'amount' => 0.00, 'description' => '', 'transaction_date' => current_time('Y-m-d') ); $data = wp_parse_args($data, $defaults); return $this->db->insert( $this->table_name, array( 'user_id' => $user_id, 'type' => sanitize_text_field($data['type']), 'category' => sanitize_text_field($data['category']), 'amount' => floatval($data['amount']), 'description' => sanitize_textarea_field($data['description']), 'transaction_date' => sanitize_text_field($data['transaction_date']) ), array('%d', '%s', '%s', '%f', '%s', '%s') ); } // 获取用户的财务记录 public function get_user_transactions($user_id, $filters = array()) { $where = array('user_id = %d'); $params = array($user_id); if (!empty($filters['type'])) { $where[] = 'type = %s'; $params[] = $filters['type']; } if (!empty($filters['category'])) { $where[] = 'category = %s'; $params[] = $filters['category']; } if (!empty($filters['start_date'])) { $where[] = 'transaction_date >= %s'; $params[] = $filters['start_date']; } if (!empty($filters['end_date'])) { $where[] = 'transaction_date <= %s'; $params[] = $filters['end_date']; } $where_clause = implode(' AND ', $where); $sql = $this->db->prepare( "SELECT * FROM {$this->table_name} WHERE {$where_clause} ORDER BY transaction_date DESC", $params ); return $this->db->get_results($sql); } // 更多方法:更新、删除、统计等 } 第三部分:用户界面与前端开发 3.1 创建管理页面 在WordPress后台添加财务管理菜单项: // 在includes/class-pfm-admin.php中 class PFM_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( '个人财务管理', '财务记账', 'read', // 所有登录用户都可以访问 'personal-finance', array($this, 'render_dashboard'), 'dashicons-chart-area', 30 ); add_submenu_page( 'personal-finance', '财务概览', '概览', 'read', 'personal-finance', array($this, 'render_dashboard') ); add_submenu_page( 'personal-finance', '添加记录', '添加记录', 'read', 'pfm-add-transaction', array($this, 'render_add_transaction_page') ); add_submenu_page( 'personal-finance', '预算管理', '预算管理', 'read', 'pfm-budgets', array($this, 'render_budgets_page') ); add_submenu_page( 'personal-finance', '财务报告', '报告', 'read', 'pfm-reports', array($this, 'render_reports_page') ); } public function render_dashboard() { include PFM_PLUGIN_DIR . 'templates/dashboard.php'; } // 其他页面渲染方法 } 3.2 前端模板设计 创建财务概览页面模板: <!-- 在templates/dashboard.php中 --> <div class="wrap pfm-dashboard"> <h1><?php echo esc_html(get_admin_page_title()); ?></h1> <div class="pfm-stats-container"> <div class="pfm-stat-card"> <h3>本月收入</h3> <div class="pfm-stat-amount income">¥<?php echo number_format($monthly_income, 2); ?></div> </div> <div class="pfm-stat-card"> <h3>本月支出</h3> <div class="pfm-stat-amount expense">¥<?php echo number_format($monthly_expense, 2); ?></div> </div> <div class="pfm-stat-card"> <h3>本月结余</h3> <div class="pfm-stat-amount balance">¥<?php echo number_format($monthly_balance, 2); ?></div> </div> <div class="pfm-stat-card"> <h3>预算执行率</h3> <div class="pfm-stat-amount"><?php echo $budget_usage; ?>%</div> </div> </div> <div class="pfm-chart-container"> <h2>月度收支趋势</h2> <canvas id="pfm-monthly-trend-chart" width="400" height="200"></canvas> </div> <div class="pfm-recent-transactions"> <h2>最近交易记录</h2> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <th>日期</th> <th>类别</th> <th>类型</th> <th>描述</th> <th>金额</th> <th>操作</th> </tr> </thead> <tbody> <?php foreach($recent_transactions as $transaction): ?> <tr> <td><?php echo esc_html($transaction->transaction_date); ?></td> <td><?php echo esc_html($transaction->category); ?></td> <td> <span class="pfm-type-badge <?php echo $transaction->type; ?>"> <?php echo $transaction->type == 'income' ? '收入' : '支出'; ?> </span> </td> <td><?php echo esc_html($transaction->description); ?></td> <td class="amount <?php echo $transaction->type; ?>"> <?php echo ($transaction->type == 'income' ? '+' : '-') . number_format($transaction->amount, 2); ?> </td> <td> <button class="button button-small edit-transaction" data-id="<?php echo $transaction->id; ?>">编辑</button> <button class="button button-small delete-transaction" data-id="<?php echo $transaction->id; ?>">删除</button> </td> </tr> <?php endforeach; ?> </tbody> </table> </div> </div> 3.3 添加记录表单 创建添加财务记录的表单界面: <!-- 在templates/add-transaction.php中 --> <div class="wrap pfm-add-transaction"> <h1>添加财务记录</h1> <form id="pfm-transaction-form" method="post"> <?php wp_nonce_field('pfm_add_transaction', 'pfm_nonce'); ?> <div class="pfm-form-section"> <h2>基本信息</h2> <div class="pfm-form-row"> <label for="transaction-type">类型</label> <select id="transaction-type" name="type" required> <option value="income">收入</option> <option value="expense" selected>支出</option> </select> </div> <div class="pfm-form-row"> <label for="transaction-category">类别</label> <select id="transaction-category" name="category" required> <option value="餐饮">餐饮</option> <option value="交通">交通</option> <option value="购物">购物</option> <option value="娱乐">娱乐</option> <option value="住房">住房</option> <option value="医疗">医疗</option> <option value="教育">教育</option> <option value="投资">投资</option> <option value="工资收入">工资收入</option> <option value="其他收入">其他收入</option> <option value="其他">其他</option> </select> <input type="text" id="new-category" name="new_category" placeholder="或输入新类别" style="display:none;"> <button type="button" id="add-category-btn" class="button">+</button> </div> <div class="pfm-form-row"> <label for="transaction-amount">金额 (¥)</label> <input type="number" id="transaction-amount" name="amount" step="0.01" min="0" required> </div> <div class="pfm-form-row"> <label for="transaction-date">日期</label> <input type="date" id="transaction-date" name="transaction_date" value="<?php echo date('Y-m-d'); ?>" required> </div> </div> <div class="pfm-form-section"> <h2>详细信息</h2> <div class="pfm-form-row"> <label for="transaction-description">描述</label> <textarea id="transaction-description" name="description" rows="3" placeholder="可选的描述信息..."></textarea> </div> <div class="pfm-form-row"> <label for="transaction-tags">标签</label> <input type="text" id="transaction-tags" name="tags" placeholder="用逗号分隔标签"> </div> </div> <div class="pfm-form-actions"> <button type="submit" class="button button-primary">保存记录</button> <button type="button" id="save-and-add-another" class="button">保存并继续添加</button> <a href="<?php echo admin_url('admin.php?page=personal-finance'); ?>" class="button">返回概览</a> </div> </form> </div> 第四部分:预算管理功能实现 4.1 预算数据模型 创建预算管理的数据模型类: // 在includes/class-pfm-budget.php中 class PFM_Budget { private $db; private $table_name; public function __construct() { global $wpdb; $this->db = $wpdb; $this->table_name = $wpdb->prefix . 'pfm_budgets'; } // 设置预算 public function set_budget($user_id, $data) { $defaults = array( 'category' => '', 'budget_amount' => 0.00, 'period' => 'monthly', 'start_date' => date('Y-m-01'), // 当月第一天 'end_date' => null ); $data = wp_parse_args($data, $defaults); // 检查是否已存在该类别同期的预算 $existing = $this->db->get_var($this->db->prepare( "SELECT id FROM {$this->table_name} WHERE user_id = %d AND category = %s AND period = %s AND start_date = %s", $user_id, $data['category'], $data['period'], $data['start_date'] )); if ($existing) { // 更新现有预算 return $this->db->update( $this->table_name, array('budget_amount' => floatval($data['budget_amount'])), array('id' => $existing), array('%f'), array('%d') ); } else { // 插入新预算 return $this->db->insert( $this->table_name, array( 'user_id' => $user_id, 'category' => sanitize_text_field($data['category']), 'budget_amount' => floatval($data['budget_amount']), 'period' => sanitize_text_field($data['period']), 'start_date' => sanitize_text_field($data['start_date']), 'end_date' => $data['end_date'] ? sanitize_text_field($data['end_date']) : null ), array('%d', '%s', '%f', '%s', '%s', '%s') ); } } // 获取用户预算执行情况 public function get_budget_progress($user_id, $period = 'current_month') { global $wpdb; // 确定日期范围 if ($period === 'current_month') { $start_date = date('Y-m-01'); $end_date = date('Y-m-t'); } elseif ($period === 'current_year') { $start_date = date('Y-01-01'); $end_date = date('Y-12-31'); } else { // 自定义日期范围 $date_range = explode('_to_', $period); $start_date = $date_range[0]; $end_date = $date_range[1]; } // 获取预算数据 $budgets = $wpdb->get_results($wpdb->prepare( "SELECT category, budget_amount, period FROM {$this->table_name} WHERE user_id = %d AND start_date <= %s AND (end_date >= %s OR end_date IS NULL)", $user_id, $end_date, $start_date )); $progress_data = array(); foreach ($budgets as $budget) { // 获取该类别在指定期间的支出总额 $transaction_table = $wpdb->prefix . 'pfm_transactions'; $actual_spent = $wpdb->get_var($wpdb->prepare( "SELECT SUM(amount) FROM {$transaction_table} WHERE user_id = %d AND category = %s AND type = 'expense' AND transaction_date BETWEEN %s AND %s", $user_id, $budget->category, $start_date, $end_date )) ?: 0; $progress_data[] = array( 'category' => $budget->category, 'budget_amount' => floatval($budget->budget_amount), 'actual_spent' => floatval($actual_spent), 'remaining' => floatval($budget->budget_amount) - floatval($actual_spent), 'usage_percentage' => $budget->budget_amount > 0 ? (floatval($actual_spent) / floatval($budget->budget_amount)) * 100 : 0 ); } return $progress_data; } } 4.2 预算管理界面 创建预算管理页面模板: <!-- 在templates/budgets.php中 --> <div class="wrap pfm-budgets"> <h1>预算管理</h1> <div class="pfm-tabs"> <button class="pfm-tab-button active" data-tab="current-budgets">当前预算</button> <button class="pfm-tab-button" data-tab="set-budget">设置预算</button> <button class="pfm-tab-button" data-tab="budget-history">历史预算</button> </div> <div id="current-budgets" class="pfm-tab-content active"> <h2>当前预算执行情况</h2> <div class="pfm-period-selector"> <label for="budget-period">查看期间:</label> <select id="budget-period"> <option value="current_month">本月</option> <option value="current_year">本年</option> <option value="last_month">上月</option> <option value="last_year">去年</option> </select> </div> <div class="pfm-budget-progress-container"> <?php foreach ($budget_progress as $item): ?> <div class="pfm-budget-item"> <div class="pfm-budget-header"> <h3><?php echo esc_html($item['category']); ?></h3> <span class="pfm-budget-amount">预算:¥<?php echo number_format($item['budget_amount'], 2); ?></span> </div> <div class="pfm-progress-bar-container"> <div class="pfm-progress-bar"> <div class="pfm-progress-fill" style="width: <?php echo min($item['usage_percentage'], 100); ?>%; background-color: <?php echo $item['usage_percentage'] > 90 ? '#f44336' : ($item['usage_percentage'] > 70 ? '#ff9800' : '#4CAF50'); ?>"> </div> </div> <div class="pfm-progress-text"> <span>已用:¥<?php echo number_format($item['actual_spent'], 2); ?></span> <span>剩余:¥<?php echo number_format($item['remaining'], 2); ?></span> <span>使用率:<?php echo number_format($item['usage_percentage'], 1); ?>%</span> </div> </div> <?php if ($item['usage_percentage'] > 100): ?> <div class="pfm-budget-alert"> <span class="dashicons dashicons-warning"></span> 已超出预算 <?php echo number_format($item['usage_percentage'] - 100, 1); ?>% </div> <?php endif; ?> </div> <?php endforeach; ?> </div> </div> <div id="set-budget" class="pfm-tab-content"> <h2>设置新预算</h2> <form id="pfm-budget-form" method="post"> <?php wp_nonce_field('pfm_set_budget', 'pfm_budget_nonce'); ?> <div class="pfm-form-row"> <label for="budget-category">预算类别</label> <select id="budget-category" name="category" required> <option value="">选择类别</option> <option value="餐饮">餐饮</option> <option value="交通">交通</option> <option value="购物">购物</option> <option value="娱乐">娱乐</option> <option value="住房">住房</option> <option value="医疗">医疗</option> <option value="教育">教育</option> <option value="其他">其他</option> </select> </div> <div class="pfm-form-row"> <label for="budget-amount">预算金额 (¥)</label> <input type="number" id="budget-amount" name="budget_amount" step="0.01" min="0" required> </div> <div class="pfm-form-row"> <label for="budget-period">预算周期</label> <select id="budget-period" name="period" required> <option value="monthly">月度预算</option> <option value="yearly">年度预算</option> <option value="custom">自定义周期</option> </select> </div> <div id="custom-period-fields" style="display: none;"> <div class="pfm-form-row"> <label for="budget-start-date">开始日期</label> <input type="date" id="budget-start-date" name="start_date"> </div> <div class="pfm-form-row"> <label for="budget-end-date">结束日期</label> <input type="date" id="budget-end-date" name="end_date"> </div> </div> <div class="pfm-form-actions"> <button type="submit" class="button button-primary">保存预算</button> <button type="reset" class="button">重置</button> </div> </form> </div> </div> 第五部分:数据可视化与报表功能 5.1 图表数据接口 创建API接口提供图表数据: // 在includes/class-pfm-api.php中 class PFM_API { public function __construct() { add_action('wp_ajax_pfm_get_chart_data', array($this, 'get_chart_data')); add_action('wp_ajax_nopriv_pfm_get_chart_data', array($this, 'require_login')); } public function get_chart_data() { // 验证用户权限 if (!is_user_logged_in()) { wp_die('请先登录'); } $user_id = get_current_user_id(); $chart_type = sanitize_text_field($_POST['chart_type']); $period = sanitize_text_field($_POST['period']); switch ($chart_type) { case 'monthly_trend': $data = $this->get_monthly_trend_data($user_id, $period); break; case 'category_distribution': $data = $this->get_category_distribution_data($user_id, $period); break; case 'budget_vs_actual': $data = $this->get_budget_vs_actual_data($user_id, $period); break; default: $data = array('error' => '无效的图表类型'); } wp_send_json_success($data); } private function get_monthly_trend_data($user_id, $months = 6) { global $wpdb; $end_date = date('Y-m-d'); $start_date = date('Y-m-d', strtotime("-$months months")); $transaction_table = $wpdb->prefix . 'pfm_transactions'; // 获取月度收入支出数据 $results = $wpdb->get_results($wpdb->prepare( "SELECT DATE_FORMAT(transaction_date, '%%Y-%%m') as month, SUM(CASE WHEN type = 'income' THEN amount ELSE 0 END) as income, SUM(CASE WHEN type = 'expense' THEN amount ELSE 0 END) as expense FROM {$transaction_table} WHERE user_id = %d AND transaction_date BETWEEN %s AND %s GROUP BY DATE_FORMAT(transaction_date, '%%Y-%%m') ORDER BY month", $user_id, $start_date, $end_date )); $labels = array(); $income_data = array(); $expense_data = array(); foreach ($results as $row) { $labels[] = $row->month; $income_data[] = floatval($row->income); $expense_data[] = floatval($row->expense); } return array( 'labels' => $labels, 'datasets' => array( array( 'label' => '收入', 'data' => $income_data, 'borderColor' => '#4CAF50', 'backgroundColor' => 'rgba(76, 175, 80, 0.1)', 'fill' => true ), array( 'label' => '支出', 'data' => $expense_data, 'borderColor' => '#f44336', 'backgroundColor' => 'rgba(244, 67, 54, 0.1)', 'fill' => true ) ) ); } private function get_category_distribution_data($user_id, $period = 'current_month') { global $wpdb; // 确定日期范围 if ($period === 'current_month') { $start_date = date('Y-m-01'); $end_date = date('Y-m-d'); } else { $start_date = date('Y-01-01'); $end_date = date('Y-12-31'); } $transaction_table = $wpdb->prefix . 'pfm_transactions'; $results = $wpdb->get_results($wpdb->prepare( "SELECT category, SUM(amount) as total FROM {$transaction_table} WHERE user_id = %d AND type = 'expense' AND transaction_date BETWEEN %s AND %s GROUP BY category ORDER BY total DESC", $user_id, $start_date, $end_date )); $labels = array(); $data = array(); $background_colors = array(); $border_colors = array(); // 预定义颜色方案 $color_palette = array( '#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40', '#8AC926', '#1982C4', '#6A4C93', '#F15BB5', '#00BBF9', '#00F5D4' ); foreach ($results as $index => $row) { $labels[] = $row->category; $data[] = floatval($row->total); $color_index = $index % count($color_palette); $background_colors[] = $color_palette[$color_index] . '80'; // 添加透明度 $border_colors[] = $color_palette[$color_index]; } return array( 'labels' => $labels, 'datasets' => array( array( 'data' => $data, 'backgroundColor' => $background_colors, 'borderColor' => $border_colors, 'borderWidth' => 1 ) ) ); } } 5.2 图表渲染与交互 创建JavaScript代码来渲染图表: // 在assets/js/pfm-charts.js中 jQuery(document).ready(function($) { // 图表实例存储 var pfmCharts = {}; // 初始化月度趋势图表 function initMonthlyTrendChart() { var ctx = document.getElementById('pfm-monthly-trend-chart').getContext('2d'); // 获取数据 $.ajax({ url: pfm_ajax.ajax_url, type: 'POST', data: { action: 'pfm_get_chart_data', chart_type: 'monthly_trend', period: '6_months', nonce: pfm_ajax.nonce }, success: function(response) { if (response.success) { pfmCharts.monthlyTrend = new Chart(ctx, { type: 'line', data: response.data, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'top', }, tooltip: { callbacks: { label: function(context) { return context.dataset.label + ': ¥' + context.parsed.y.toFixed(2); } } } }, scales: { y: { beginAtZero: true, ticks: { callback: function(value) { return '¥' + value; } } } } } }); } } }); } // 初始化分类分布图表 function initCategoryDistributionChart() { var ctx = document.getElementById('pfm-category-chart').getContext('2d'); $.ajax({ url: pfm_ajax.ajax_url, type: 'POST', data: { action: 'pfm_get_chart_data', chart_type: 'category_distribution', period: 'current_month', nonce: pfm_ajax.nonce }, success: function(response) { if (response.success) { pfmCharts.categoryDistribution = new Chart(ctx, { type: 'doughnut', data: response.data, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', }, tooltip: { callbacks: { label: function(context) { var label = context.label || ''; var value = context.parsed || 0; var total = context.dataset.data.reduce((a, b) => a + b, 0); var percentage = Math.round((value / total) * 100); return label + ': ¥' + value.toFixed(2) + ' (' + percentage + '%)'; } } } } } }); } } }); } // 更新图表数据 function updateChart(chartType, period) { if (pfmCharts[chartType]) { $.ajax({ url: pfm_ajax.ajax_url, type: 'POST', data: { action: 'pfm_get_chart_data', chart_type: chartType, period: period, nonce: pfm_ajax.nonce }, success: function(response) { if (response.success) { pfmCharts[chartType].data = response.data; pfmCharts[chartType].update(); } } }); } } // 初始化所有图表 if ($('#pfm-monthly-trend-chart').length) { initMonthlyTrendChart(); } if ($('#pfm-category-chart').length) { initCategoryDistributionChart(); } // 图表控制事件 $('.pfm-chart-controls select').on('change', function() { var chartType = $(this).data('chart-type'); var period = $(this).val(); updateChart(chartType, period); }); }); 5.3 报表生成功能 创建报表生成功能: // 在includes/class-pfm-reports.php中 class PFM_Reports { public function generate_monthly_report($user_id, $year, $month) { global $wpdb; $start_date = sprintf('%04d-%02d-01', $year, $month); $end_date = date('Y-m-t', strtotime($start_date)); $transaction_table = $wpdb->prefix . 'pfm_transactions'; $budget_table = $wpdb->prefix . 'pfm_budgets'; // 获取月度汇总数据 $summary = $wpdb->get_row($wpdb->prepare( "SELECT

发表评论

手把手教学,为WordPress集成智能化的网站表单数据清洗与去重工具

手把手教学:为WordPress集成智能化的网站表单数据清洗与去重工具 引言:为什么WordPress网站需要数据清洗与去重工具 在当今数字化时代,网站表单已成为企业与用户互动、收集信息的重要渠道。无论是联系表单、注册表单、调查问卷还是订单提交,WordPress网站每天都会通过各类表单收集大量用户数据。然而,这些数据往往存在各种质量问题:重复提交、格式不规范、信息不完整甚至包含恶意内容。 数据质量问题不仅影响后续的数据分析和业务决策,还可能导致营销资源浪费、客户体验下降以及系统性能问题。例如,同一用户多次提交相同信息会占用数据库空间,发送重复邮件可能引起用户反感,而不规范的数据格式则使数据分析变得困难。 传统上,WordPress管理员需要手动处理这些问题,耗时耗力且容易出错。因此,为WordPress集成智能化的数据清洗与去重工具显得尤为重要。本文将手把手教您如何通过WordPress代码二次开发,实现这一常用互联网小工具功能。 第一部分:理解WordPress表单数据处理机制 1.1 WordPress表单插件的工作原理 WordPress本身不提供强大的表单功能,大多数网站都依赖第三方表单插件,如Contact Form 7、Gravity Forms、WPForms等。这些插件通常遵循相似的工作流程: 前端渲染表单HTML 用户填写并提交表单 服务器端验证和处理数据 存储数据到数据库或发送到指定邮箱 显示成功或错误消息 理解这一流程是进行二次开发的基础。大多数表单插件都提供了丰富的钩子(Hooks)和过滤器(Filters),允许开发者在关键节点插入自定义代码。 1.2 WordPress数据存储方式分析 WordPress表单数据通常以以下几种方式存储: 自定义数据库表:专业表单插件如Gravity Forms会创建专用表存储提交数据 文章类型(Post Type):部分插件将表单提交存储为自定义文章类型 选项(Options):简单表单可能将数据存储在wp_options表中 直接发送到邮箱:不存储数据库,仅通过邮件发送 了解数据存储方式对于设计清洗和去重策略至关重要。我们的工具需要能够适应不同的存储方式,提供统一的处理接口。 第二部分:设计智能化数据清洗与去重系统 2.1 系统架构设计 一个完整的数据清洗与去重系统应包含以下模块: 数据采集模块:从不同来源收集表单数据 预处理模块:标准化数据格式 清洗规则引擎:根据预设规则清洗数据 去重算法模块:识别和合并重复数据 数据验证模块:验证数据有效性和完整性 日志与报告模块:记录处理过程和结果 管理界面模块:提供后台配置和监控界面 2.2 关键技术选择 在WordPress环境中实现这一系统,我们需要选择合适的技术方案: PHP作为主要开发语言:WordPress核心使用PHP,所有插件和主题都基于PHP MySQL数据库操作:直接操作WordPress数据库进行数据查询和更新 WordPress钩子系统:利用Action和Filter钩子集成到现有表单处理流程 JavaScript/AJAX:增强前端交互体验 正则表达式:用于复杂的数据格式匹配和清洗 2.3 数据清洗规则设计 数据清洗规则应根据具体业务需求定制,常见规则包括: 格式标准化:统一电话号码、日期、邮箱等格式 去除无效字符:清理HTML标签、特殊字符等 拼写校正:常见词汇的自动校正 缺失值处理:识别并标记或填充缺失数据 异常值检测:识别不符合业务逻辑的数据 敏感信息过滤:移除或加密敏感个人信息 2.4 去重算法设计 去重算法需要平衡准确性和性能: 精确匹配:完全相同的记录 模糊匹配:考虑拼写错误、缩写、同义词等 基于规则的匹配:根据业务规则定义重复条件 机器学习方法:使用相似度算法识别潜在重复 在WordPress环境中,考虑到性能限制,我们主要采用前三种方法。 第三部分:手把手实现数据清洗功能 3.1 创建WordPress插件框架 首先,我们创建一个基本的WordPress插件框架: <?php /** * Plugin Name: 智能表单数据清洗与去重工具 * Plugin URI: https://yourwebsite.com/ * Description: 为WordPress表单提供智能数据清洗和去重功能 * Version: 1.0.0 * Author: 你的名字 * License: GPL v2 or later */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('SFDCD_VERSION', '1.0.0'); define('SFDCD_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('SFDCD_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 class Smart_Form_Data_Cleaner { 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() { // 初始化钩子 add_action('plugins_loaded', array($this, 'load_textdomain')); add_action('admin_menu', array($this, 'add_admin_menu')); add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts')); // 表单处理钩子 - 以Contact Form 7为例 add_filter('wpcf7_posted_data', array($this, 'clean_cf7_data'), 10, 1); add_action('wpcf7_before_send_mail', array($this, 'check_duplicates_cf7'), 10, 3); } public function load_textdomain() { load_plugin_textdomain('smart-form-data-cleaner', false, dirname(plugin_basename(__FILE__)) . '/languages'); } // 其他方法将在后续步骤中添加 } // 启动插件 Smart_Form_Data_Cleaner::get_instance(); 3.2 实现基础数据清洗功能 接下来,我们实现基础的数据清洗功能: class Data_Cleaner_Engine { /** * 清洗表单数据 * @param array $form_data 原始表单数据 * @return array 清洗后的数据 */ public static function clean_form_data($form_data) { if (!is_array($form_data) || empty($form_data)) { return $form_data; } $cleaned_data = array(); foreach ($form_data as $field => $value) { // 跳过空值 if (is_null($value) || $value === '') { $cleaned_data[$field] = $value; continue; } // 根据字段类型应用不同的清洗规则 $field_type = self::detect_field_type($field, $value); $cleaned_value = self::apply_cleaning_rules($value, $field_type); $cleaned_data[$field] = $cleaned_value; } return $cleaned_data; } /** * 检测字段类型 */ private static function detect_field_type($field_name, $value) { $field_name = strtolower($field_name); // 根据字段名关键词判断类型 if (strpos($field_name, 'email') !== false) { return 'email'; } elseif (strpos($field_name, 'phone') !== false || strpos($field_name, 'tel') !== false) { return 'phone'; } elseif (strpos($field_name, 'date') !== false) { return 'date'; } elseif (strpos($field_name, 'name') !== false) { return 'name'; } elseif (strpos($field_name, 'address') !== false) { return 'address'; } elseif (strpos($field_name, 'url') !== false || strpos($field_name, 'website') !== false) { return 'url'; } else { // 根据值内容进一步判断 if (is_email($value)) { return 'email'; } elseif (preg_match('/^d{10,}$/', preg_replace('/[^0-9]/', '', $value))) { return 'phone'; } elseif (strtotime($value) !== false) { return 'date'; } else { return 'text'; } } } /** * 应用清洗规则 */ private static function apply_cleaning_rules($value, $field_type) { // 通用清洗:去除首尾空格、HTML标签、多余空白字符 $value = trim($value); $value = strip_tags($value); $value = preg_replace('/s+/', ' ', $value); // 根据字段类型应用特定清洗规则 switch ($field_type) { case 'email': $value = self::clean_email($value); break; case 'phone': $value = self::clean_phone($value); break; case 'date': $value = self::clean_date($value); break; case 'name': $value = self::clean_name($value); break; case 'url': $value = self::clean_url($value); break; case 'address': $value = self::clean_address($value); break; default: // 对普通文本进行基本清洗 $value = self::clean_text($value); } return $value; } /** * 清洗邮箱地址 */ private static function clean_email($email) { $email = strtolower(trim($email)); // 移除邮箱地址中的多余字符 $email = filter_var($email, FILTER_SANITIZE_EMAIL); // 验证邮箱格式 if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { // 尝试修复常见邮箱错误 $email = preg_replace('/s+/', '', $email); $email = preg_replace('/[,;]/', '.', $email); // 再次验证 if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { // 如果仍然无效,记录错误但保留原值 error_log('无效的邮箱格式: ' . $email); } } return $email; } /** * 清洗电话号码 */ private static function clean_phone($phone) { // 移除非数字字符 $digits = preg_replace('/[^0-9]/', '', $phone); // 根据不同国家/地区格式标准化 $country_code = self::detect_phone_country_code($digits); switch ($country_code) { case '86': // 中国 if (strlen($digits) === 11) { return preg_replace('/(d{3})(d{4})(d{4})/', '$1 $2 $3', $digits); } break; case '1': // 美国/加拿大 if (strlen($digits) === 10) { return preg_replace('/(d{3})(d{3})(d{4})/', '($1) $2-$3', $digits); } elseif (strlen($digits) === 11) { return preg_replace('/(d{1})(d{3})(d{3})(d{4})/', '+$1 ($2) $3-$4', $digits); } break; default: // 通用格式化:每3-4位加空格 if (strlen($digits) > 6) { $formatted = ''; while (strlen($digits) > 4) { $formatted .= substr($digits, 0, 3) . ' '; $digits = substr($digits, 3); } $formatted .= $digits; return $formatted; } } return $phone; // 如果无法标准化,返回原值 } // 其他清洗方法... } 3.3 集成到Contact Form 7 现在我们将清洗引擎集成到Contact Form 7中: // 在Smart_Form_Data_Cleaner类中添加以下方法 public function clean_cf7_data($posted_data) { // 应用数据清洗 $cleaned_data = Data_Cleaner_Engine::clean_form_data($posted_data); // 记录清洗日志 $this->log_cleaning_process($posted_data, $cleaned_data); return $cleaned_data; } public function check_duplicates_cf7($contact_form, &$abort, $submission) { $posted_data = $submission->get_posted_data(); // 检查重复提交 $is_duplicate = $this->check_for_duplicates($posted_data, 'cf7'); if ($is_duplicate) { // 如果是重复数据,可以阻止提交或标记为重复 $abort = true; $submission->set_response(__('检测到重复提交,请勿重复提交表单。', 'smart-form-data-cleaner')); // 或者可以选择不阻止,但记录重复 // $this->log_duplicate_submission($posted_data); } } private function check_for_duplicates($form_data, $form_type) { global $wpdb; // 根据表单类型确定检查策略 $threshold_time = apply_filters('sfdcd_duplicate_threshold', 24 * HOUR_IN_SECONDS); // 默认24小时内 // 构建查询条件 $conditions = array(); $values = array(); // 检查关键字段:邮箱、电话等 foreach ($form_data as $field => $value) { if ($this->is_key_field($field)) { $conditions[] = "meta_value LIKE %s"; $values[] = '%' . $wpdb->esc_like($value) . '%'; } } if (empty($conditions)) { return false; } // 查询数据库检查重复 $query = "SELECT COUNT(*) FROM {$wpdb->prefix}postmeta WHERE meta_key IN ('_form_data', '_submission_data') AND (" . implode(' OR ', $conditions) . ") AND post_id IN ( SELECT ID FROM {$wpdb->prefix}posts WHERE post_type = 'wpcf7_contact_form' AND post_date > DATE_SUB(NOW(), INTERVAL %d SECOND) )"; $values[] = $threshold_time; $count = $wpdb->get_var($wpdb->prepare($query, $values)); return $count > 0; } private function is_key_field($field_name) { $key_fields = array('email', 'phone', 'tel', 'mobile', 'e-mail'); foreach ($key_fields as $key) { if (stripos($field_name, $key) !== false) { return true; } } return false; } 第四部分:实现智能去重功能 4.1 设计去重算法 去重功能比简单清洗更复杂,需要智能识别相似但不完全相同的数据: class Deduplication_Engine { /** * 查找重复记录 */ public static function find_duplicates($new_data, $form_type, $threshold = 0.8) { global $wpdb; // 获取历史数据进行比较 $historical_data = self::get_historical_data($form_type); $duplicates = array(); foreach ($historical_data as $record) { $similarity = self::calculate_similarity($new_data, $record['data']); if ($similarity >= $threshold) { $duplicates[] = array( 'id' => $record['id'], 'similarity' => $similarity, 'data' => $record['data'] ); } } return $duplicates; } /** * 计算两条记录之间的相似度 */ private static function calculate_similarity($data1, $data2) { $total_weight = 0; $similarity_score = 0; // 定义字段权重 $field_weights = array( 'email' => 0.4, 'phone' => 0.3, 'name' => 0.2, 'address' => 0.1 ); foreach ($data1 as $field => $value1) { if (!isset($data2[$field])) { continue; } $value2 = $data2[$field]; // 确定字段类型和权重 $field_type = Data_Cleaner_Engine::detect_field_type($field, $value1); $weight = isset($field_weights[$field_type]) ? $field_weights[$field_type] : 0.05; // 计算字段相似度 $field_similarity = self::compare_field_values($value1, $value2, $field_type); $similarity_score += $field_similarity * $weight; $total_weight += $weight; } // 避免除以零 if ($total_weight == 0) { return 0; } return $similarity_score / $total_weight; } /** * 比较字段值相似度 */ private static function compare_field_values($value1, $value2, $field_type) { if ($value1 === $value2) { return 1.0; } // 空值处理 if (empty($value1) || empty($value2)) { return 0.2; // 部分匹配分数 } switch ($field_type) { case 'email': // 邮箱比较:考虑用户名和域名分开比较 list($user1, $domain1) = explode('@', strtolower($value1)); list($user2, $domain2) = explode('@', strtolower($value2)); $user_similarity = self::string_similarity($user1, $user2); $domain_similarity = ($domain1 === $domain2) ? 1.0 : 0; return ($user_similarity * 0.7) + ($domain_similarity * 0.3); case 'phone': // 电话号码比较:提取数字后比较 $digits1 = preg_replace('/[^0-9]/', '', $value1); $digits2 = preg_replace('/[^0-9]/', '', $value2); // 考虑国际区号差异 if (strlen($digits1) > 10 && strlen($digits2) > 10) { // 都有国际区号,比较后8-10位 $compare1 = substr($digits1, -10); $compare2 = substr($digits2, -10); return ($compare1 === $compare2) ? 0.9 : 0; } else { // 直接比较 return ($digits1 === $digits2) ? 1.0 : self::string_similarity($digits1, $digits2); } case 'name': // 姓名比较:考虑中英文、顺序、缩写 return self::name_similarity($value1, $value2); default: // 普通文本比较 return self::string_similarity($value1, $value2); } } /** * 字符串相似度计算(使用Levenshtein距离) */ private static function string_similarity($str1, $str2) { $len1 = strlen($str1); $len2 = strlen($str2); if ($len1 === 0 || $len2 === 0) { return 0; } // 计算Levenshtein距离 $distance = levenshtein(strtolower($str1), strtolower($str2)); $max_len = max($len1, $len2); return 1 - ($distance / $max_len); } /** * 姓名相似度计算 */ private static function name_similarity($name1, $name2) { // 转换为小写 $name1 = strtolower(trim($name1)); $name2 = strtolower(trim($name2)); // 完全相等 if ($name1 === $name2) { return 1.0; } // 分割姓名组成部分 $parts1 = preg_split('/[s.,]+/', $name1); $parts2 = preg_split('/[s.,]+/', $name2); // 检查是否只是顺序不同 if (count(array_intersect($parts1, $parts2)) === count($parts1) && count($parts1) === count($parts2)) { return 0.9; } // 检查缩写匹配 $initials1 = array_map(function($part) { return substr($part, 0, 1); }, $parts1); $initials2 = array_map(function($part) { return substr($part, 0, 1); }, $parts2); if (implode('', $initials1) === implode('', $initials2)) { return 0.8; } // 计算平均相似度 $total_similarity = 0; $comparisons = 0; foreach ($parts1 as $part1) { foreach ($parts2 as $part2) { $similarity = self::string_similarity($part1, $part2); if ($similarity > 0.7) { $total_similarity += $similarity; $comparisons++; } } } if ($comparisons > 0) { return $total_similarity / $comparisons; } return self::string_similarity($name1, $name2); } /** * 获取历史数据 */ private static function get_historical_data($form_type, $limit = 100) { global $wpdb; $table_name = $wpdb->prefix . 'sfdcd_submissions'; // 检查表是否存在,如果不存在则返回空数组 if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") != $table_name) { return array(); } $query = $wpdb->prepare( "SELECT id, form_data FROM $table_name WHERE form_type = %s ORDER BY submission_date DESC LIMIT %d", $form_type, $limit ); $results = $wpdb->get_results($query, ARRAY_A); $historical_data = array(); foreach ($results as $row) { $historical_data[] = array( 'id' => $row['id'], 'data' => maybe_unserialize($row['form_data']) ); } return $historical_data; } } ### 4.2 创建数据存储表 为了有效进行去重,我们需要创建专门的表来存储表单提交记录: class Database_Manager { /** * 创建数据表 */ public static function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'sfdcd_submissions'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id bigint(20) NOT NULL AUTO_INCREMENT, form_type varchar(100) NOT NULL, form_id varchar(100) DEFAULT NULL, form_data longtext NOT NULL, cleaned_data longtext NOT NULL, duplicate_check_hash varchar(64) DEFAULT NULL, is_duplicate tinyint(1) DEFAULT 0, duplicate_of bigint(20) DEFAULT NULL, submission_date datetime DEFAULT CURRENT_TIMESTAMP, processed_date datetime DEFAULT NULL, PRIMARY KEY (id), KEY form_type (form_type), KEY duplicate_check_hash (duplicate_check_hash), KEY submission_date (submission_date), KEY is_duplicate (is_duplicate) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 创建清洗规则表 $rules_table = $wpdb->prefix . 'sfdcd_cleaning_rules'; $sql = "CREATE TABLE IF NOT EXISTS $rules_table ( id bigint(20) NOT NULL AUTO_INCREMENT, rule_name varchar(200) NOT NULL, rule_type varchar(50) NOT NULL, field_pattern varchar(200) DEFAULT NULL, condition text NOT NULL, action text NOT NULL, priority int(11) DEFAULT 10, is_active tinyint(1) DEFAULT 1, created_date datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY rule_type (rule_type), KEY is_active (is_active) ) $charset_collate;"; dbDelta($sql); } /** * 存储表单提交 */ public static function store_submission($form_type, $form_id, $original_data, $cleaned_data) { global $wpdb; $table_name = $wpdb->prefix . 'sfdcd_submissions'; // 生成去重检查哈希 $hash_data = self::prepare_data_for_hashing($cleaned_data); $duplicate_hash = md5(serialize($hash_data)); // 检查是否已存在相同哈希的记录 $existing = $wpdb->get_var($wpdb->prepare( "SELECT id FROM $table_name WHERE duplicate_check_hash = %s AND form_type = %s AND submission_date > DATE_SUB(NOW(), INTERVAL 7 DAY)", $duplicate_hash, $form_type )); $is_duplicate = $existing ? 1 : 0; $duplicate_of = $existing ? $existing : null; // 插入新记录 $result = $wpdb->insert( $table_name, array( 'form_type' => $form_type, 'form_id' => $form_id, 'form_data' => maybe_serialize($original_data), 'cleaned_data' => maybe_serialize($cleaned_data), 'duplicate_check_hash' => $duplicate_hash, 'is_duplicate' => $is_duplicate, 'duplicate_of' => $duplicate_of, 'processed_date' => current_time('mysql') ), array('%s', '%s', '%s', '%s', '%s', '%d', '%d', '%s') ); return array( 'success' => $result !== false, 'submission_id' => $wpdb->insert_id, 'is_duplicate' => $is_duplicate, 'duplicate_of' => $duplicate_of ); } /** * 准备用于哈希计算的数据 */ private static function prepare_data_for_hashing($data) { $hash_data = array(); // 只选择关键字段进行哈希计算 $key_fields = array('email', 'phone', 'name', 'user_login', 'e-mail'); foreach ($data as $field => $value) { $field_lower = strtolower($field); foreach ($key_fields as $key) { if (strpos($field_lower, $key) !== false) { $hash_data[$field] = $value; break; } } } // 如果没有关键字段,使用所有字段 if (empty($hash_data)) { $hash_data = $data; } // 对值进行标准化处理 foreach ($hash_data as &$value) { if (is_string($value)) { $value = strtolower(trim($value)); $value = preg_replace('/s+/', ' ', $value); } } // 按键名排序确保一致性 ksort($hash_data); return $hash_data; } } ## 第五部分:创建管理界面 ### 5.1 后台管理菜单 // 在Smart_Form_Data_Cleaner类中添加管理界面方法 public function add_admin_menu() { // 主菜单 add_menu_page( __('智能表单数据清洗', 'smart-form-data-cleaner'), __('数据清洗', 'smart-form-data-cleaner'), 'manage_options', 'smart-form-data-cleaner', array($this, 'render_main_page'), 'dashicons-filter', 30 ); // 子菜单 add_submenu_page( 'smart-form-data-cleaner', __('数据清洗记录', 'smart-form-data-cleaner'), __('清洗记录', 'smart-form-data-cleaner'), 'manage_options', 'sfdcd-cleaning-logs', array($this, 'render_cleaning_logs_page') ); add_submenu_page( 'smart-form-data-cleaner', __('重复数据管理', 'smart-form-data-cleaner'), __('重复数据', 'smart-form-data-cleaner'), 'manage_options', 'sfdcd-duplicates', array($this, 'render_duplicates_page') ); add_submenu_page( 'smart-form-data-cleaner', __('清洗规则设置', 'smart-form-data-cleaner'), __('清洗规则', 'smart-form-data-cleaner'), 'manage_options', 'sfdcd-cleaning-rules', array($this, 'render_rules_page') ); add_submenu_page( 'smart-form-data-cleaner', __('系统设置', 'smart-form-data-cleaner'), __('系统设置', 'smart-form-data-cleaner'), 'manage_options', 'sfdcd-settings', array($this, 'render_settings_page') ); } public function enqueue_admin_scripts($hook) { // 只在我们插件页面加载资源 if (strpos($hook, 'smart-form-data-cleaner') === false && strpos($hook, 'sfdcd-') === false) { return; } // 加载CSS wp_enqueue_style( 'sfdcd-admin-style', SFDCD_PLUGIN_URL . 'assets/css/admin.css', array(), SFDCD_VERSION ); // 加载JavaScript wp_enqueue_script( 'sfdcd-admin-script', SFDCD_PLUGIN_URL . 'assets/js/admin.js', array('jquery', 'jquery-ui-sortable', 'jquery-ui-dialog'), SFDCD_VERSION, true ); // 本地化脚本 wp_localize_script('sfdcd-admin-script', 'sfdcd_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('sfdcd_ajax_nonce'), 'confirm_delete' => __('确定要删除这条记录吗?', 'smart-form-data-cleaner'), 'processing' => __('处理中...', 'smart-form-data-cleaner') )); } ### 5.2 主页面模板 public function render_main_page() { // 检查数据库表是否存在,如果不存在则创建 Database_Manager::create_tables(); // 获取统计数据 $stats = $this->get_system_stats(); ?> <div class="wrap sfdcd-dashboard"> <h1><?php _e('智能表单数据清洗与去重工具', 'smart-form-data-cleaner'); ?></h1> <div class="sfdcd-stats-container"> <div class="sfdcd-stat-card"> <h3><?php _e('今日提交', 'smart-form-data-cleaner'); ?></h3> <div class="stat-number"><?php echo esc_html($stats['today_submissions']); ?></div> </div> <div class="sfdcd-stat-card"> <h3><?php _e('重复数据', 'smart-form-data-cleaner'); ?></h3> <div class="stat-number"><?php echo esc_html($stats['duplicate_count']); ?></div> <div class="stat-percentage"><?php echo esc_html($stats['duplicate_percentage']); ?>%</div> </div> <div class="sfdcd-stat-card"> <h3><?php _e('数据清洗', 'smart-form-data-cleaner'); ?></h3> <div class="stat-number"><?php echo esc_html($stats['cleaned_count']); ?></div> <div class="stat-percentage"><?php echo esc_html($stats['cleaning_rate']); ?>%</div> </div> <div class="sfdcd-stat-card"> <h3><?php _e('活跃规则', 'smart-form-data-cleaner'); ?></h3> <div class="stat-number"><?php echo esc_html($stats['active_rules']); ?></div> </div> </div> <div class="sfdcd-dashboard-content"> <div class="sfdcd-dashboard-column"> <div class="sfdcd-card"> <h2><?php _e('最近提交', 'smart-form-data-cleaner'); ?></h2> <?php $this->render_recent_submissions(); ?> </div> <div class="sfdcd-card"> <h2><?php _e('快速操作', 'smart-form-data-cleaner'); ?></h2> <div class="sfdcd-quick-actions"> <button class="button button-primary" onclick="location.href='?page=sfdcd-cleaning-rules&action=add'"> <?php _e('添加清洗规则', 'smart-form-data-cleaner'); ?> </button> <button class="button" onclick="sfdcd_run_manual_cleanup()"> <?php _e('手动清洗数据', 'smart-form-data-cleaner'); ?> </button> <button class="button" onclick="sfdcd_check_duplicates()"> <?php _e('检查重复数据', 'smart-form-data-cleaner'); ?> </button> <button class="button" onclick="location.href='?page=sfdcd-settings'"> <?php _e('系统设置', 'smart-form-data-cleaner'); ?> </button> </div> </div> </div> <div class="sfdcd-dashboard-column"> <div class="sfdcd-card"> <h2><?php _e('系统状态', 'smart-form-data-cleaner'); ?></h2> <div class="sfdcd-system-status"> <div class="status-item"> <span class="status-label"><?php _e('数据库表', 'smart-form-data-cleaner'); ?>:</span> <span class="status-value status-ok"><?php _e('正常', 'smart-form-data-cleaner'); ?></span> </div> <div class="status-item"> <span class="status-label"><?php _e('最后运行', 'smart-form-data-cleaner'); ?>:</span> <span class="status-value"><?php echo esc_html($stats['last_run']); ?></span> </div> <div class="status-item"> <span class="status-label"><?php _e('支持的表单插件', 'smart-form-data-cleaner'); ?>:</

发表评论

详细指南,开发网站会员签到打卡与连续奖励积分系统

详细指南:开发网站会员签到打卡与连续奖励积分系统 摘要 在当今互联网时代,用户参与度和忠诚度是网站成功的关键因素之一。会员签到打卡与连续奖励积分系统作为一种有效的用户激励工具,已被广泛应用于各类网站和应用程序中。本文将详细介绍如何通过WordPress程序的代码二次开发,实现一个功能完善的会员签到打卡与连续奖励积分系统。我们将从系统设计、数据库结构、核心功能实现、用户界面设计到系统测试与优化,全面解析开发过程,帮助开发者快速掌握这一常用互联网小工具的实现方法。 第一章:系统概述与设计思路 1.1 签到打卡系统的价值 签到打卡系统是一种简单而有效的用户参与机制,通过每日签到获取积分奖励的方式,鼓励用户养成访问网站的习惯。这种系统不仅提高了用户粘性,还能促进内容消费和社区互动。结合连续签到奖励机制,用户为了获得更高的积分奖励,会努力保持连续签到记录,从而形成良性的用户行为循环。 1.2 系统功能需求分析 一个完整的会员签到打卡与连续奖励积分系统应包含以下核心功能: 用户签到功能:允许用户每日签到一次,记录签到时间 连续签到追踪:跟踪用户连续签到天数,并在中断时重置 积分奖励机制:根据签到情况奖励相应积分,连续签到天数越多奖励越高 签到日历展示:可视化展示用户的签到历史记录 奖励规则配置:管理员可灵活配置积分奖励规则 用户数据统计:展示用户的签到统计信息 积分兑换功能:允许用户使用积分兑换虚拟或实物奖励 1.3 WordPress二次开发的优势 WordPress作为全球最流行的内容管理系统,拥有以下优势: 成熟的用户管理系统 丰富的插件架构和钩子机制 庞大的开发者社区支持 灵活的主题和插件扩展能力 良好的安全性和稳定性 通过WordPress二次开发实现签到系统,可以充分利用现有用户体系,减少重复开发工作,提高开发效率。 第二章:数据库设计与数据模型 2.1 数据表结构设计 为了实现签到打卡系统,我们需要在WordPress数据库中添加以下自定义数据表: -- 用户签到记录表 CREATE TABLE wp_signin_records ( id INT(11) NOT NULL AUTO_INCREMENT, user_id INT(11) NOT NULL, signin_date DATE NOT NULL, signin_time DATETIME NOT NULL, continuous_days INT(11) DEFAULT 1, points_earned INT(11) DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY user_date (user_id, signin_date), KEY user_id (user_id), KEY signin_date (signin_date) ); -- 用户积分总表 CREATE TABLE wp_user_points ( id INT(11) NOT NULL AUTO_INCREMENT, user_id INT(11) NOT NULL, total_points INT(11) DEFAULT 0, available_points INT(11) DEFAULT 0, last_updated DATETIME NOT NULL, PRIMARY KEY (id), UNIQUE KEY user_id (user_id) ); -- 积分奖励规则表 CREATE TABLE wp_points_rules ( id INT(11) NOT NULL AUTO_INCREMENT, rule_name VARCHAR(100) NOT NULL, continuous_days INT(11) NOT NULL, points_award INT(11) NOT NULL, is_active TINYINT(1) DEFAULT 1, PRIMARY KEY (id) ); -- 积分兑换记录表 CREATE TABLE wp_points_redemption ( id INT(11) NOT NULL AUTO_INCREMENT, user_id INT(11) NOT NULL, redemption_date DATETIME NOT NULL, points_used INT(11) NOT NULL, item_name VARCHAR(200) NOT NULL, status VARCHAR(50) DEFAULT 'pending', PRIMARY KEY (id), KEY user_id (user_id) ); 2.2 数据关系与索引优化 为了提高查询效率,我们在关键字段上建立了索引: wp_signin_records表的user_id和signin_date字段建立联合唯一索引,防止重复签到 各表的user_id字段建立普通索引,加快用户相关查询 signin_date字段建立索引,便于按日期范围查询统计 2.3 与WordPress用户表的关联 我们的签到系统需要与WordPress核心用户表wp_users进行关联,通过user_id字段建立外键关系,确保数据一致性。同时,我们可以利用WordPress的用户元数据表wp_usermeta存储一些额外的用户签到相关信息。 第三章:核心功能实现 3.1 签到功能模块 3.1.1 签到逻辑实现 <?php /** * 处理用户签到 */ function handle_user_signin($user_id) { global $wpdb; // 检查用户是否已签到 $today = date('Y-m-d'); $table_name = $wpdb->prefix . 'signin_records'; $already_signed = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $table_name WHERE user_id = %d AND signin_date = %s", $user_id, $today )); if ($already_signed > 0) { return array( 'success' => false, 'message' => '今日已签到,请明天再来!' ); } // 获取用户昨日签到记录,计算连续签到天数 $yesterday = date('Y-m-d', strtotime('-1 day')); $yesterday_record = $wpdb->get_row($wpdb->prepare( "SELECT continuous_days FROM $table_name WHERE user_id = %d AND signin_date = %s", $user_id, $yesterday )); $continuous_days = 1; if ($yesterday_record) { $continuous_days = $yesterday_record->continuous_days + 1; } // 根据连续签到天数计算奖励积分 $points_earned = calculate_signin_points($continuous_days); // 插入签到记录 $result = $wpdb->insert( $table_name, array( 'user_id' => $user_id, 'signin_date' => $today, 'signin_time' => current_time('mysql'), 'continuous_days' => $continuous_days, 'points_earned' => $points_earned ), array('%d', '%s', '%s', '%d', '%d') ); if ($result) { // 更新用户总积分 update_user_points($user_id, $points_earned); return array( 'success' => true, 'message' => '签到成功!', 'continuous_days' => $continuous_days, 'points_earned' => $points_earned ); } else { return array( 'success' => false, 'message' => '签到失败,请稍后重试!' ); } } /** * 根据连续签到天数计算奖励积分 */ function calculate_signin_points($continuous_days) { $base_points = 10; // 基础积分 // 连续签到额外奖励 $extra_points = 0; if ($continuous_days >= 7) { $extra_points = 30; } elseif ($continuous_days >= 30) { $extra_points = 100; } elseif ($continuous_days >= 100) { $extra_points = 500; } // 特殊日期额外奖励(如节假日) $special_bonus = check_special_date_bonus(); return $base_points + $extra_points + $special_bonus; } ?> 3.1.2 AJAX签到接口 <?php /** * 注册AJAX签到接口 */ add_action('wp_ajax_user_signin', 'ajax_handle_signin'); add_action('wp_ajax_nopriv_user_signin', 'ajax_handle_signin_no_priv'); function ajax_handle_signin() { // 验证用户登录状态 if (!is_user_logged_in()) { wp_send_json_error('请先登录!'); } $user_id = get_current_user_id(); $result = handle_user_signin($user_id); if ($result['success']) { wp_send_json_success($result); } else { wp_send_json_error($result['message']); } } function ajax_handle_signin_no_priv() { wp_send_json_error('请先登录!'); } ?> 3.2 连续签到追踪模块 <?php /** * 获取用户连续签到天数 */ function get_user_continuous_days($user_id) { global $wpdb; $table_name = $wpdb->prefix . 'signin_records'; $today = date('Y-m-d'); // 获取今日签到记录 $today_record = $wpdb->get_row($wpdb->prepare( "SELECT continuous_days FROM $table_name WHERE user_id = %d AND signin_date = %s", $user_id, $today )); if ($today_record) { return $today_record->continuous_days; } // 如果今日未签到,检查昨日是否签到 $yesterday = date('Y-m-d', strtotime('-1 day')); $yesterday_record = $wpdb->get_row($wpdb->prepare( "SELECT continuous_days FROM $table_name WHERE user_id = %d AND signin_date = %s", $user_id, $yesterday )); if ($yesterday_record) { return 0; // 今日未签到,但昨日已签到,连续天数中断 } // 检查连续签到是否中断 return check_continuous_break($user_id); } /** * 检查连续签到是否中断 */ function check_continuous_break($user_id) { global $wpdb; $table_name = $wpdb->prefix . 'signin_records'; // 获取最近一次签到记录 $latest_record = $wpdb->get_row($wpdb->prepare( "SELECT signin_date, continuous_days FROM $table_name WHERE user_id = %d ORDER BY signin_date DESC LIMIT 1", $user_id )); if (!$latest_record) { return 0; // 从未签到 } $last_signin_date = $latest_record->signin_date; $days_since_last = floor((time() - strtotime($last_signin_date)) / (60 * 60 * 24)); // 如果距离上次签到超过1天,则连续签到中断 if ($days_since_last > 1) { return 0; } return $latest_record->continuous_days; } ?> 3.3 积分管理系统 <?php /** * 更新用户积分 */ function update_user_points($user_id, $points_to_add) { global $wpdb; $table_name = $wpdb->prefix . 'user_points'; // 检查用户积分记录是否存在 $existing_record = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE user_id = %d", $user_id )); if ($existing_record) { // 更新现有记录 $new_total = $existing_record->total_points + $points_to_add; $new_available = $existing_record->available_points + $points_to_add; $wpdb->update( $table_name, array( 'total_points' => $new_total, 'available_points' => $new_available, 'last_updated' => current_time('mysql') ), array('user_id' => $user_id), array('%d', '%d', '%s'), array('%d') ); } else { // 创建新记录 $wpdb->insert( $table_name, array( 'user_id' => $user_id, 'total_points' => $points_to_add, 'available_points' => $points_to_add, 'last_updated' => current_time('mysql') ), array('%d', '%d', '%d', '%s') ); } // 记录积分变动日志 log_points_change($user_id, $points_to_add, 'signin_reward', '签到奖励'); } /** * 积分兑换功能 */ function redeem_points($user_id, $item_id, $points_required) { global $wpdb; // 检查用户可用积分 $user_points = $wpdb->get_row($wpdb->prepare( "SELECT available_points FROM {$wpdb->prefix}user_points WHERE user_id = %d", $user_id )); if (!$user_points || $user_points->available_points < $points_required) { return array( 'success' => false, 'message' => '积分不足,无法兑换!' ); } // 获取兑换物品信息 $item_info = get_redemption_item($item_id); if (!$item_info) { return array( 'success' => false, 'message' => '兑换物品不存在!' ); } // 开始事务处理 $wpdb->query('START TRANSACTION'); try { // 扣除用户积分 $wpdb->query($wpdb->prepare( "UPDATE {$wpdb->prefix}user_points SET available_points = available_points - %d WHERE user_id = %d", $points_required, $user_id )); // 记录兑换记录 $wpdb->insert( $wpdb->prefix . 'points_redemption', array( 'user_id' => $user_id, 'redemption_date' => current_time('mysql'), 'points_used' => $points_required, 'item_name' => $item_info['name'], 'status' => 'pending' ), array('%d', '%s', '%d', '%s', '%s') ); $wpdb->query('COMMIT'); return array( 'success' => true, 'message' => '兑换成功,请等待管理员处理!' ); } catch (Exception $e) { $wpdb->query('ROLLBACK'); return array( 'success' => false, 'message' => '兑换失败:' . $e->getMessage() ); } } ?> 第四章:用户界面设计与实现 4.1 签到界面设计 <?php /** * 签到界面短代码 */ add_shortcode('signin_system', 'display_signin_system'); function display_signin_system() { if (!is_user_logged_in()) { return '<div class="signin-notice">请先登录以使用签到功能</div>'; } $user_id = get_current_user_id(); $user_info = get_userdata($user_id); $continuous_days = get_user_continuous_days($user_id); $today_signed = check_today_signin($user_id); ob_start(); ?> <div class="signin-container"> <div class="signin-header"> <h2>每日签到</h2> <p>连续签到可获得更多积分奖励</p> </div> <div class="user-stats"> <div class="stat-item"> <span class="stat-label">用户名</span> <span class="stat-value"><?php echo esc_html($user_info->display_name); ?></span> </div> <div class="stat-item"> <span class="stat-label">连续签到</span> <span class="stat-value"><?php echo $continuous_days; ?> 天</span> </div> <div class="stat-item"> <span class="stat-label">总积分</span> <span class="stat-value"><?php echo get_user_total_points($user_id); ?></span> </div> </div> <div class="signin-main"> <?php if ($today_signed): ?> <div class="signin-already"> <div class="signin-icon">✓</div> <h3>今日已签到</h3> <p>您已连续签到 <?php echo $continuous_days; ?> 天</p> <p>明日再来可获得更多积分!</p> </div> <?php else: ?> <div class="signin-available" id="signin-box"> <div class="signin-icon">📅</div> <h3>立即签到</h3> <p>今日签到可获得 <span class="points-preview"><?php echo calculate_signin_points($continuous_days + 1); ?></span> 积分</p> <button class="signin-button" id="signin-button">立即签到</button> <div class="signin-loading" id="signin-loading" style="display:none;"> 处理中... </div> </div> <?php endif; ?> </div> <div class="signin-calendar"> <h3>签到日历</h3> <?php display_signin_calendar($user_id); ?> </div> <div class="rewards-info"> <h3>连续签到奖励规则</h3> <ul class="rewards-list"> <li>第1-6天:每天10积分</li> <li>第7天:额外奖励30积分</li> <li>第30天:额外奖励100积分</li> <li>第100天:额外奖励500积分</li> <li>节假日:额外奖励20积分</li> </ul> </div> </div> <script> jQuery(document).ready(function($) { $('#signin-button').click(function() { var button = $(this); var loading = $('#signin-loading'); button.prop('disabled', true).text('签到中...'); loading.show(); $.ajax({ url: '<?php echo admin_url('admin-ajax.php'); ?>', type: 'POST', data: { action: 'user_signin', nonce: '<?php echo wp_create_nonce('signin_nonce'); ?>' }, success: function(response) { if (response.success) { // 更新界面显示 $('#signin-box').html(` <div class="signin-success"> <div class="signin-icon">🎉</div> <h3>签到成功!</h3> <p>获得 <span class="points-earned">${response.data.points_earned}</span> 积分</p> <p>连续签到天数:${response.data.continuous_days} 天</p> <p>明日签到可获得更多积分!</p> </div> `); // 更新用户统计信息 setTimeout(function() { location.reload(); }, 2000); } else { alert(response.data); button.prop('disabled', false).text('立即签到'); loading.hide(); } }, error: function() { alert('签到失败,请稍后重试!'); button.prop('disabled', false).text('立即签到'); loading.hide(); } }); }); }); </script> <?php return ob_get_clean(); } /** 显示签到日历 */ function display_signin_calendar($user_id) { global $wpdb; $table_name = $wpdb->prefix . 'signin_records'; $current_month = date('n'); $current_year = date('Y'); // 获取本月签到记录 $signin_records = $wpdb->get_results($wpdb->prepare( "SELECT DAY(signin_date) as day, points_earned FROM $table_name WHERE user_id = %d AND MONTH(signin_date) = %d AND YEAR(signin_date) = %d", $user_id, $current_month, $current_year )); // 创建签到记录映射 $signin_map = array(); foreach ($signin_records as $record) { $signin_map[$record->day] = $record->points_earned; } // 生成日历 $first_day = mktime(0, 0, 0, $current_month, 1, $current_year); $days_in_month = date('t', $first_day); $first_day_of_week = date('w', $first_day); ob_start(); ?> <div class="calendar"> <div class="calendar-header"> <span class="month-year"><?php echo date('Y年n月'); ?></span> </div> <div class="calendar-weekdays"> <div>日</div> <div>一</div> <div>二</div> <div>三</div> <div>四</div> <div>五</div> <div>六</div> </div> <div class="calendar-days"> <?php // 填充空白 for ($i = 0; $i < $first_day_of_week; $i++) { echo '<div class="empty"></div>'; } // 填充日期 for ($day = 1; $day <= $days_in_month; $day++) { $is_today = ($day == date('j')); $has_signed = isset($signin_map[$day]); $class = 'day'; if ($is_today) $class .= ' today'; if ($has_signed) $class .= ' signed'; echo '<div class="' . $class . '">'; echo '<span class="day-number">' . $day . '</span>'; if ($has_signed) { echo '<span class="signin-indicator" title="获得' . $signin_map[$day] . '积分">✓</span>'; } echo '</div>'; } ?> </div> </div> <?php return ob_get_clean(); }?> ### 4.2 用户积分中心 <?php/** 用户积分中心短代码 */ add_shortcode('points_center', 'display_points_center'); function display_points_center() { if (!is_user_logged_in()) { return '<div class="points-center-notice">请先登录以查看积分中心</div>'; } $user_id = get_current_user_id(); $points_data = get_user_points_data($user_id); $redemption_items = get_redemption_items(); $points_history = get_points_history($user_id, 10); ob_start(); ?> <div class="points-center-container"> <div class="points-overview"> <div class="points-card"> <h3>我的积分</h3> <div class="points-total"> <span class="points-number"><?php echo $points_data['available_points']; ?></span> <span class="points-label">可用积分</span> </div> <div class="points-details"> <div class="detail-item"> <span>总获得积分</span> <span><?php echo $points_data['total_points']; ?></span> </div> <div class="detail-item"> <span>已使用积分</span> <span><?php echo $points_data['total_points'] - $points_data['available_points']; ?></span> </div> </div> </div> <div class="points-actions"> <h3>快速操作</h3> <button class="btn-checkin" onclick="location.href='#signin-system'">每日签到</button> <button class="btn-history" id="view-history">查看积分记录</button> <button class="btn-redeem" id="show-redeem-modal">积分兑换</button> </div> </div> <div class="points-history-section"> <h3>最近积分记录</h3> <table class="points-history-table"> <thead> <tr> <th>日期</th> <th>类型</th> <th>积分变动</th> <th>说明</th> </tr> </thead> <tbody> <?php foreach ($points_history as $record): ?> <tr> <td><?php echo date('Y-m-d H:i', strtotime($record->change_time)); ?></td> <td><?php echo get_points_change_type_label($record->change_type); ?></td> <td class="<?php echo $record->points_change >= 0 ? 'points-increase' : 'points-decrease'; ?>"> <?php echo ($record->points_change >= 0 ? '+' : '') . $record->points_change; ?> </td> <td><?php echo esc_html($record->description); ?></td> </tr> <?php endforeach; ?> </tbody> </table> </div> <div class="redemption-section"> <h3>积分兑换商城</h3> <div class="redemption-items"> <?php foreach ($redemption_items as $item): ?> <div class="redemption-item"> <div class="item-image"> <img src="<?php echo esc_url($item['image']); ?>" alt="<?php echo esc_attr($item['name']); ?>"> </div> <div class="item-info"> <h4><?php echo esc_html($item['name']); ?></h4> <p class="item-description"><?php echo esc_html($item['description']); ?></p> <div class="item-points"> <span class="points-required"><?php echo $item['points_required']; ?> 积分</span> <?php if ($points_data['available_points'] >= $item['points_required']): ?> <button class="btn-redeem-item" data-item-id="<?php echo $item['id']; ?>" data-item-name="<?php echo esc_attr($item['name']); ?>" data-points-required="<?php echo $item['points_required']; ?>"> 立即兑换 </button> <?php else: ?> <button class="btn-redeem-item disabled" disabled>积分不足</button> <?php endif; ?> </div> </div> </div> <?php endforeach; ?> </div> </div> </div> <!-- 兑换确认模态框 --> <div id="redeem-modal" class="modal"> <div class="modal-content"> <span class="close-modal">&times;</span> <h3>确认兑换</h3> <div id="redeem-item-info"></div> <div class="modal-actions"> <button id="confirm-redeem" class="btn-confirm">确认兑换</button> <button id="cancel-redeem" class="btn-cancel">取消</button> </div> </div> </div> <script> jQuery(document).ready(function($) { // 兑换按钮点击事件 $('.btn-redeem-item').click(function() { var itemId = $(this).data('item-id'); var itemName = $(this).data('item-name'); var pointsRequired = $(this).data('points-required'); $('#redeem-item-info').html(` <p>您要兑换:<strong>${itemName}</strong></p> <p>需要积分:<strong>${pointsRequired}</strong></p> <p>兑换后积分将立即扣除,兑换物品将在3个工作日内处理。</p> `); $('#redeem-modal').data('item-id', itemId); $('#redeem-modal').data('points-required', pointsRequired); $('#redeem-modal').show(); }); // 确认兑换 $('#confirm-redeem').click(function() { var itemId = $('#redeem-modal').data('item-id'); var pointsRequired = $('#redeem-modal').data('points-required'); $.ajax({ url: '<?php echo admin_url('admin-ajax.php'); ?>', type: 'POST', data: { action: 'redeem_points', item_id: itemId, points_required: pointsRequired, nonce: '<?php echo wp_create_nonce('redeem_nonce'); ?>' }, success: function(response) { if (response.success) { alert('兑换成功!'); location.reload(); } else { alert('兑换失败:' + response.data); } }, error: function() { alert('兑换失败,请稍后重试!'); } }); }); // 关闭模态框 $('.close-modal, #cancel-redeem').click(function() { $('#redeem-modal').hide(); }); }); </script> <?php return ob_get_clean(); }?> --- ## 第五章:后台管理功能 ### 5.1 管理菜单与界面 <?php/** 添加管理菜单 */ add_action('admin_menu', 'register_signin_admin_menu'); function register_signin_admin_menu() { add_menu_page( '签到管理系统', '签到管理', 'manage_options', 'signin-management', 'display_signin_admin_page', 'dashicons-calendar-alt', 30 ); add_submenu_page( 'signin-management', '签到记录', '签到记录', 'manage_options', 'signin-records', 'display_signin_records_page' ); add_submenu_page( 'signin-management', '积分规则', '积分规则', 'manage_options', 'points-rules', 'display_points_rules_page' ); add_submenu_page( 'signin-management', '兑换管理', '兑换管理', 'manage_options', 'redemption-management', 'display_redemption_management_page' ); add_submenu_page( 'signin-management', '数据统计', '数据统计', 'manage_options', 'signin-statistics', 'display_signin_statistics_page' ); } /** 显示签到记录管理页面 */ function display_signin_records_page() { global $wpdb; $table_name = $wpdb->prefix . 'signin_records'; $users_table = $wpdb->users; // 处理搜索和筛选 $search = isset($_GET['s']) ? sanitize_text_field($_GET['s']) : ''; $date_from = isset($_GET['date_from']) ? sanitize_text_field($_GET['date_from']) : ''; $date_to = isset($_GET['date_to']) ? sanitize_text_field($_GET['date_to']) : ''; // 构建查询条件 $where = array('1=1'); $query_params = array(); if (!empty($search)) { $where[] = "(u.user_login LIKE %s OR u.display_name LIKE %s)"; $query_params[] = '%' . $wpdb->esc_like($search) . '%'; $query_params[] = '%' . $wpdb->esc_like($search) . '%'; } if (!empty($date_from)) { $where[] = "s.signin_date >= %s"; $query_params[] = $date_from; } if (!empty($date_to)) { $where[] = "s.signin_date <= %s"; $query_params[] = $date_to; } $where_clause = implode(' AND ', $where); // 获取总记录数 $total_query = "SELECT COUNT(*) FROM $table_name s INNER JOIN $users_table u ON s.user_id = u.ID WHERE $where_clause"; if (!empty($query_params)) { $total_query = $wpdb->prepare($total_query, $query_params); } $total_items = $wpdb->get_var($total_query); // 分页设置 $per_page = 20; $current_page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1; $offset = ($current_page - 1) * $per_page; // 获取记录数据 $query = "SELECT s.*, u.user_login, u.display_name FROM $table_name s INNER JOIN $users_table u ON s.user_id = u.ID WHERE $where_clause ORDER BY s.signin_date DESC, s.signin_time DESC LIMIT %d OFFSET %d"; $query_params[] = $per_page; $query_params[] = $offset; $records = $wpdb->get_results($wpdb->prepare($query, $query_params)); ob_start(); ?> <div class="wrap"> <h1 class="wp-heading-inline">签到记录管理</h1> <form method="get" action="" class="search-form"> <input type="hidden" name="page" value="signin-records"> <div class="tablenav top"> <div class="alignleft actions"> <input type="text" name="s" value="<?php echo esc_attr($search); ?>" placeholder="搜索用户名或显示名称"> <label>从:<input type="date" name="date_from" value="<?php echo esc_attr($date_from); ?>"></label> <label>到:<input type="date" name="date_to" value="<?php echo esc_attr($date_to); ?>"></label> <input type="submit" class="button" value="筛选"> <?php if (!empty($search) || !empty($date_from) || !empty($date_to)): ?> <a href="?page=signin-records" class="button">清除筛选</a> <?php endif; ?> </div> <div class="tablenav-pages"> <?php $total_pages = ceil($total_items / $per_page); echo paginate_links(array( 'base' => add_query_arg('paged', '%#%'), 'format' => '', 'prev_text' => '&laquo;', 'next_text' => '&raquo;', 'total' => $total_pages, 'current' => $current_page )); ?> </div> </div> </form> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <th>用户</th> <th>签到日期</th> <th>签到时间</th> <th>连续天数</th> <th>获得积分</th> <th>操作</th> </tr> </thead> <tbody> <?php if (empty($records)): ?> <tr> <td colspan="6" style="text-align: center;">暂无签到记录</td> </tr> <?php else: ?> <?php foreach ($records as $record): ?> <tr> <td> <strong><?php echo esc_html($record->display_name); ?></strong><br> <small><?php echo esc_html($record->user_login); ?></small> </td> <td><?php echo esc_html($record->signin_date); ?></td> <td><?php echo esc_html($record->signin_time); ?></td> <td><?php echo esc_html($record->continuous_days); ?></td> <td><?php echo esc_html($record->points_earned); ?></td> <td> <button class="button button-small" onclick="deleteSigninRecord(<?php echo $record->id; ?

发表评论

一步步实现,为WordPress打造内嵌的在线流程图绘制与原型设计工具

一步步实现:为WordPress打造内嵌的在线流程图绘制与原型设计工具 引言:为什么WordPress需要内置流程图与原型设计工具? 在当今数字化时代,可视化沟通已成为网站开发、项目管理和内容创作中不可或缺的一环。无论是网站架构规划、用户流程设计,还是产品原型展示,流程图和原型设计工具都能显著提高工作效率和沟通效果。 然而,大多数WordPress用户目前面临一个共同困境:当需要在文章中插入流程图或原型设计时,他们不得不依赖外部工具(如Lucidchart、Figma或Draw.io)创建图表,然后以图片形式导入WordPress。这种方法存在明显缺陷:无法直接编辑、图片质量损失、缺乏交互性,并且破坏了内容创作的一体化体验。 本文将通过详细的代码实现步骤,展示如何为WordPress打造一个完全内嵌的在线流程图绘制与原型设计工具,让用户无需离开WordPress后台即可创建、编辑和发布专业的可视化图表。 第一部分:项目规划与技术选型 1.1 功能需求分析 在开始编码之前,我们需要明确工具的核心功能: 基本绘图功能:支持常见流程图元素(矩形、圆形、菱形、箭头等) 原型设计组件:按钮、输入框、导航栏等UI元素库 实时协作:支持多用户同时编辑(可选高级功能) 导出与分享:支持PNG、SVG、JSON等多种格式导出 WordPress集成:与文章编辑器无缝对接,支持短代码嵌入 版本控制:保存编辑历史,支持版本回退 响应式设计:确保在不同设备上都能良好工作 1.2 技术架构设计 考虑到开发效率和功能完整性,我们采用混合技术方案: 前端绘图库:使用开源的Draw.io(mxGraph)核心库,这是一个功能强大且成熟的图形绘制库 WordPress集成:通过自定义插件方式集成到WordPress 数据存储:使用WordPress自定义数据表结合文章元数据 后端API:REST API处理图表保存、加载和用户权限管理 1.3 开发环境准备 在开始开发前,确保你的环境满足以下要求: WordPress 5.0+(支持Gutenberg编辑器) PHP 7.4+ MySQL 5.6+ 基本的Web开发知识(HTML、CSS、JavaScript、PHP) 第二部分:创建WordPress插件基础框架 2.1 初始化插件文件结构 首先,在WordPress的wp-content/plugins/目录下创建插件文件夹wp-flowchart-designer,并建立以下文件结构: wp-flowchart-designer/ ├── wp-flowchart-designer.php # 主插件文件 ├── includes/ │ ├── class-database.php # 数据库处理类 │ ├── class-shortcode.php # 短代码处理器 │ ├── class-rest-api.php # REST API处理器 │ └── class-admin.php # 后台管理类 ├── admin/ │ ├── css/ │ │ └── admin-style.css # 后台样式 │ └── js/ │ └── admin-script.js # 后台脚本 ├── public/ │ ├── css/ │ │ └── public-style.css # 前台样式 │ ├── js/ │ │ ├── public-script.js # 前台脚本 │ │ └── mxgraph/ # mxGraph库文件 │ └── partials/ # 公共模板部分 ├── assets/ # 静态资源 └── uninstall.php # 插件卸载脚本 2.2 编写主插件文件 创建主插件文件wp-flowchart-designer.php: <?php /** * Plugin Name: WordPress流程图与原型设计工具 * Plugin URI: https://yourwebsite.com/wp-flowchart-designer * Description: 为WordPress添加内嵌的在线流程图绘制与原型设计功能 * Version: 1.0.0 * Author: 你的名字 * License: GPL v2 or later * Text Domain: wp-flowchart-designer */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('WPFD_VERSION', '1.0.0'); define('WPFD_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('WPFD_PLUGIN_URL', plugin_dir_url(__FILE__)); define('WPFD_PLUGIN_BASENAME', plugin_basename(__FILE__)); // 自动加载类文件 spl_autoload_register(function ($class_name) { $prefix = 'WPFD_'; $base_dir = WPFD_PLUGIN_DIR . 'includes/'; $len = strlen($prefix); if (strncmp($prefix, $class_name, $len) !== 0) { return; } $relative_class = substr($class_name, $len); $file = $base_dir . 'class-' . strtolower(str_replace('_', '-', $relative_class)) . '.php'; if (file_exists($file)) { require $file; } }); // 初始化插件 function wpfd_init() { // 检查WordPress版本 if (version_compare(get_bloginfo('version'), '5.0', '<')) { add_action('admin_notices', function() { echo '<div class="notice notice-error"><p>'; echo __('WordPress流程图与原型设计工具需要WordPress 5.0或更高版本。', 'wp-flowchart-designer'); echo '</p></div>'; }); return; } // 初始化各个组件 WPFD_Database::init(); WPFD_Shortcode::init(); WPFD_REST_API::init(); if (is_admin()) { WPFD_Admin::init(); } } add_action('plugins_loaded', 'wpfd_init'); // 激活插件时创建数据库表 register_activation_hook(__FILE__, ['WPFD_Database', 'create_tables']); // 卸载插件时清理数据 register_uninstall_hook(__FILE__, ['WPFD_Database', 'drop_tables']); 第三部分:实现数据库层 3.1 创建数据库表 在includes/class-database.php中,我们创建存储图表数据的数据表: <?php class WPFD_Database { private static $table_name; public static function init() { global $wpdb; self::$table_name = $wpdb->prefix . 'wpfd_diagrams'; } public static function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE IF NOT EXISTS " . self::$table_name . " ( id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, post_id bigint(20) UNSIGNED DEFAULT NULL, user_id bigint(20) UNSIGNED NOT NULL, title varchar(255) NOT NULL, diagram_data longtext NOT NULL, diagram_type varchar(50) DEFAULT 'flowchart', settings text, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY post_id (post_id), KEY user_id (user_id), KEY diagram_type (diagram_type) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 添加版本选项,便于后续升级 add_option('wpfd_db_version', '1.0'); } public static function drop_tables() { global $wpdb; $sql = "DROP TABLE IF EXISTS " . self::$table_name; $wpdb->query($sql); delete_option('wpfd_db_version'); } public static function save_diagram($data) { global $wpdb; $defaults = [ 'post_id' => null, 'user_id' => get_current_user_id(), 'title' => '未命名图表', 'diagram_data' => '', 'diagram_type' => 'flowchart', 'settings' => '{}' ]; $data = wp_parse_args($data, $defaults); if (isset($data['id']) && $data['id'] > 0) { // 更新现有图表 $wpdb->update( self::$table_name, [ 'title' => sanitize_text_field($data['title']), 'diagram_data' => wp_json_encode($data['diagram_data']), 'diagram_type' => sanitize_text_field($data['diagram_type']), 'settings' => wp_json_encode($data['settings']), 'updated_at' => current_time('mysql') ], ['id' => $data['id']], ['%s', '%s', '%s', '%s', '%s'], ['%d'] ); return $data['id']; } else { // 插入新图表 $wpdb->insert( self::$table_name, [ 'post_id' => $data['post_id'], 'user_id' => $data['user_id'], 'title' => sanitize_text_field($data['title']), 'diagram_data' => wp_json_encode($data['diagram_data']), 'diagram_type' => sanitize_text_field($data['diagram_type']), 'settings' => wp_json_encode($data['settings']), 'created_at' => current_time('mysql'), 'updated_at' => current_time('mysql') ], ['%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s'] ); return $wpdb->insert_id; } } public static function get_diagram($id) { global $wpdb; $result = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM " . self::$table_name . " WHERE id = %d", $id ), ARRAY_A ); if ($result) { $result['diagram_data'] = json_decode($result['diagram_data'], true); $result['settings'] = json_decode($result['settings'], true); } return $result; } public static function get_user_diagrams($user_id = null, $limit = 20, $offset = 0) { global $wpdb; if (!$user_id) { $user_id = get_current_user_id(); } $results = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM " . self::$table_name . " WHERE user_id = %d ORDER BY updated_at DESC LIMIT %d OFFSET %d", $user_id, $limit, $offset ), ARRAY_A ); foreach ($results as &$result) { $result['diagram_data'] = json_decode($result['diagram_data'], true); $result['settings'] = json_decode($result['settings'], true); } return $results; } public static function delete_diagram($id) { global $wpdb; return $wpdb->delete( self::$table_name, ['id' => $id], ['%d'] ); } } 第四部分:集成Draw.io编辑器 4.1 引入mxGraph库 由于Draw.io基于mxGraph库,我们需要将其集成到插件中。可以从GitHub获取mxGraph库: 访问 https://github.com/jgraph/mxgraph 下载最新版本 将javascript目录复制到public/js/mxgraph/ 4.2 创建编辑器界面 在admin/js/admin-script.js中,我们创建编辑器初始化代码: (function($) { 'use strict'; // 全局编辑器实例 var wpfdEditor = null; // 初始化流程图编辑器 function initFlowchartEditor(containerId, initialData) { // 检查mxGraph库是否已加载 if (typeof mxClient === 'undefined') { console.error('mxGraph库未加载'); return; } // 创建编辑器容器 var container = document.getElementById(containerId); if (!container) { console.error('找不到容器: ' + containerId); return; } // 禁用右键菜单 mxEvent.disableContextMenu(container); // 创建图形模型 var model = new mxGraphModel(); var graph = new mxGraph(container, model); // 配置图形 graph.setConnectable(true); graph.setMultigraph(false); graph.setAllowDanglingEdges(false); graph.setDropEnabled(true); graph.setPanning(true); graph.setTooltips(true); // 启用缩放 new mxKeyHandler(graph); new mxRubberband(graph); // 设置样式 var style = graph.getStylesheet().getDefaultVertexStyle(); style[mxConstants.STYLE_FONTFAMILY] = 'Helvetica, Arial, sans-serif'; style[mxConstants.STYLE_FONTSIZE] = '12'; style[mxConstants.STYLE_STROKECOLOR] = '#2D3748'; style[mxConstants.STYLE_FILLCOLOR] = '#EDF2F7'; // 加载初始数据 if (initialData && initialData.xml) { try { var doc = mxUtils.parseXml(initialData.xml); var codec = new mxCodec(doc); codec.decode(doc.documentElement, graph.getModel()); } catch (e) { console.error('解析图表数据失败:', e); } } // 保存编辑器实例 wpfdEditor = { graph: graph, model: model, container: container }; return wpfdEditor; } // 获取图表数据 function getDiagramData() { if (!wpfdEditor) { return null; } var encoder = new mxCodec(); var node = encoder.encode(wpfdEditor.model); return { xml: mxUtils.getXml(node), bounds: wpfdEditor.graph.getGraphBounds(), cells: wpfdEditor.model.getDescendants(wpfdEditor.model.getRoot()) }; } // 导出为图片 function exportAsImage(format, bgColor) { if (!wpfdEditor) { return null; } var bounds = wpfdEditor.graph.getGraphBounds(); var scale = wpfdEditor.graph.view.scale; // 创建临时canvas var canvas = document.createElement('canvas'); canvas.width = bounds.width * scale; canvas.height = bounds.height * scale; var imgExport = new mxImageExport(); var ctx = canvas.getContext('2d'); // 设置背景色 if (bgColor) { ctx.fillStyle = bgColor; ctx.fillRect(0, 0, canvas.width, canvas.height); } // 导出图形 imgExport.drawState(wpfdEditor.graph.getView().getState(wpfdEditor.model.getRoot()), ctx); return canvas.toDataURL('image/' + format); } // 添加形状到工具栏 function addShapeToToolbar(toolbarId, shapeConfig) { var toolbar = document.getElementById(toolbarId); if (!toolbar) return; var button = document.createElement('button'); button.type = 'button'; button.className = 'wpfd-toolbar-btn'; button.innerHTML = shapeConfig.icon || shapeConfig.label; button.title = shapeConfig.label; button.addEventListener('click', function() { if (!wpfdEditor) return; wpfdEditor.graph.stopEditing(); var parent = wpfdEditor.graph.getDefaultParent(); wpfdEditor.model.beginUpdate(); try { var vertex = wpfdEditor.graph.insertVertex( parent, null, shapeConfig.label, 20, 20, shapeConfig.width || 120, shapeConfig.height || 60, shapeConfig.style || '' ); // 将新形状置于视图中心 var geo = wpfdEditor.model.getGeometry(vertex); var bounds = wpfdEditor.graph.getGraphBounds(); geo.x = Math.max(20, (bounds.width - geo.width) / 2); geo.y = Math.max(20, (bounds.height - geo.height) / 2); wpfdEditor.graph.setSelectionCell(vertex); } finally { wpfdEditor.model.endUpdate(); } }); toolbar.appendChild(button); } // 初始化工具栏 function initToolbar(toolbarId) { // 预定义形状 var shapes = [ { label: '开始/结束', style: 'shape=ellipse', width: 100, height: 60 }, { label: '过程', style: '', width: 120, height: 60 }, { label: '判断', style: 'shape=rhombus', width: 100, height: 80 }, { label: '数据', style: 'shape=parallelogram', width: 120, height: 60 }, { label: '文档', style: 'shape=document', width: 100, height: 80 }, { label: '子流程', style: 'shape=process;perimeter=rectanglePerimeter', width: 140, height: 80 } ]; shapes.forEach(function(shape) { addShapeToToolbar(toolbarId, shape); }); } // WordPress集成 $(document).ready(function() { // 初始化编辑器 if ($('#wpfd-editor-container').length) { var initialData = window.wpfdInitialData || {}; wpfdEditor = initFlowchartEditor('wpfd-editor-container', initialData); ('#wpfd-toolbar').length) { initToolbar('wpfd-toolbar'); } } // 保存图表 $('#wpfd-save-diagram').on('click', function() { var diagramData = getDiagramData(); var title = $('#wpfd-diagram-title').val() || '未命名图表'; if (!diagramData) { alert('无法获取图表数据'); return; } // 显示保存中状态 var $button = $(this); var originalText = $button.text(); $button.text('保存中...').prop('disabled', true); // 发送保存请求 $.ajax({ url: wpfd_ajax.ajax_url, type: 'POST', data: { action: 'wpfd_save_diagram', nonce: wpfd_ajax.nonce, title: title, diagram_data: JSON.stringify(diagramData), diagram_type: 'flowchart' }, success: function(response) { if (response.success) { alert('图表保存成功!'); if (response.data.redirect) { window.location.href = response.data.redirect; } } else { alert('保存失败: ' + response.data.message); } }, error: function() { alert('保存请求失败,请检查网络连接'); }, complete: function() { $button.text(originalText).prop('disabled', false); } }); }); // 导出功能 $('.wpfd-export-btn').on('click', function() { var format = $(this).data('format'); var imageData = exportAsImage(format, '#ffffff'); if (imageData) { // 创建下载链接 var link = document.createElement('a'); link.download = 'diagram.' + format; link.href = imageData; document.body.appendChild(link); link.click(); document.body.removeChild(link); } }); // 插入到文章 $('#wpfd-insert-to-post').on('click', function() { var diagramData = getDiagramData(); if (!diagramData) { alert('无法获取图表数据'); return; } // 生成短代码 var shortcode = '[wpfd_diagram id="' + wpfd_ajax.diagram_id + '"]'; // 发送到编辑器 if (typeof wp !== 'undefined' && wp.media && wp.media.editor) { wp.media.editor.insert(shortcode); } else { // 备用方案:复制到剪贴板 var tempInput = document.createElement('input'); tempInput.value = shortcode; document.body.appendChild(tempInput); tempInput.select(); document.execCommand('copy'); document.body.removeChild(tempInput); alert('短代码已复制到剪贴板'); } }); }); // 暴露公共API window.WPFD_Editor = { init: initFlowchartEditor, getData: getDiagramData, exportImage: exportAsImage, addShape: addShapeToToolbar }; })(jQuery); ### 4.3 创建编辑器CSS样式 在`admin/css/admin-style.css`中添加编辑器样式: / 流程图编辑器主容器 /.wpfd-editor-wrapper { display: flex; flex-direction: column; height: 800px; border: 1px solid #ccd0d4; border-radius: 4px; overflow: hidden; background: #f8f9fa; } / 编辑器工具栏 /.wpfd-editor-toolbar { background: #fff; border-bottom: 1px solid #ccd0d4; padding: 10px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-height: 60px; } .wpfd-toolbar-group { display: flex; align-items: center; gap: 4px; padding: 0 10px; border-right: 1px solid #e0e0e0; } .wpfd-toolbar-group:last-child { border-right: none; } .wpfd-toolbar-btn { background: #f0f0f0; border: 1px solid #ddd; border-radius: 3px; padding: 6px 12px; cursor: pointer; font-size: 13px; color: #333; transition: all 0.2s; display: flex; align-items: center; gap: 5px; } .wpfd-toolbar-btn:hover { background: #e0e0e0; border-color: #ccc; } .wpfd-toolbar-btn.active { background: #007cba; color: white; border-color: #007cba; } .wpfd-toolbar-btn i { font-size: 16px; } / 编辑器主区域 /.wpfd-editor-main { display: flex; flex: 1; overflow: hidden; } / 左侧形状面板 /.wpfd-shapes-panel { width: 200px; background: #fff; border-right: 1px solid #ccd0d4; padding: 15px; overflow-y: auto; } .wpfd-shapes-category { margin-bottom: 20px; } .wpfd-shapes-category h4 { margin: 0 0 10px 0; font-size: 13px; color: #666; text-transform: uppercase; font-weight: 600; } .wpfd-shapes-list { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; } .wpfd-shape-item { background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 4px; padding: 10px; cursor: move; text-align: center; font-size: 12px; transition: all 0.2s; } .wpfd-shape-item:hover { background: #e9ecef; border-color: #007cba; } / 画布区域 /.wpfd-canvas-container { flex: 1; position: relative; overflow: hidden; background: #fff; } wpfd-editor-container { width: 100%; height: 100%; min-height: 600px; } / 右侧属性面板 /.wpfd-properties-panel { width: 300px; background: #fff; border-left: 1px solid #ccd0d4; padding: 15px; overflow-y: auto; } .wpfd-property-group { margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #eee; } .wpfd-property-group:last-child { border-bottom: none; margin-bottom: 0; } .wpfd-property-group h4 { margin: 0 0 12px 0; font-size: 14px; color: #333; font-weight: 600; } .wpfd-property-item { margin-bottom: 10px; } .wpfd-property-item label { display: block; margin-bottom: 5px; font-size: 13px; color: #555; } .wpfd-property-item input[type="text"],.wpfd-property-item input[type="number"],.wpfd-property-item select { width: 100%; padding: 6px 8px; border: 1px solid #ddd; border-radius: 3px; font-size: 13px; } .wpfd-property-item input[type="color"] { width: 100%; height: 35px; padding: 2px; border: 1px solid #ddd; border-radius: 3px; } / 编辑器底部状态栏 /.wpfd-editor-statusbar { background: #fff; border-top: 1px solid #ccd0d4; padding: 8px 15px; font-size: 12px; color: #666; display: flex; justify-content: space-between; align-items: center; } / 响应式设计 /@media (max-width: 1200px) { .wpfd-editor-wrapper { height: 600px; } .wpfd-shapes-panel { width: 180px; } .wpfd-properties-panel { width: 250px; } } @media (max-width: 768px) { .wpfd-editor-main { flex-direction: column; } .wpfd-shapes-panel { width: 100%; height: 150px; border-right: none; border-bottom: 1px solid #ccd0d4; } .wpfd-properties-panel { width: 100%; height: 200px; border-left: none; border-top: 1px solid #ccd0d4; } .wpfd-shapes-list { grid-template-columns: repeat(4, 1fr); } } / 图标字体 /@font-face { font-family: 'wpfd-icons'; src: url('../fonts/wpfd-icons.woff2') format('woff2'), url('../fonts/wpfd-icons.woff') format('woff'); font-weight: normal; font-style: normal; } .wpfd-icon { font-family: 'wpfd-icons'; speak: never; font-style: normal; font-weight: normal; font-variant: normal; text-transform: none; line-height: 1; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .wpfd-icon-rectangle:before { content: "e900"; }.wpfd-icon-circle:before { content: "e901"; }.wpfd-icon-diamond:before { content: "e902"; }.wpfd-icon-arrow:before { content: "e903"; }.wpfd-icon-text:before { content: "e904"; }.wpfd-icon-line:before { content: "e905"; }.wpfd-icon-save:before { content: "e906"; }.wpfd-icon-export:before { content: "e907"; }.wpfd-icon-undo:before { content: "e908"; }.wpfd-icon-redo:before { content: "e909"; }.wpfd-icon-zoom-in:before { content: "e90a"; }.wpfd-icon-zoom-out:before { content: "e90b"; } ## 第五部分:实现REST API接口 ### 5.1 创建REST API处理器 在`includes/class-rest-api.php`中实现API接口: <?phpclass WPFD_REST_API { public static function init() { add_action('rest_api_init', [__CLASS__, 'register_routes']); } public static function register_routes() { // 图表CRUD接口 register_rest_route('wpfd/v1', '/diagrams', [ [ 'methods' => 'GET', 'callback' => [__CLASS__, 'get_diagrams'], 'permission_callback' => [__CLASS__, 'check_permission'], 'args' => [ 'per_page' => [ 'default' => 20, 'validate_callback' => function($param) { return is_numeric($param) && $param > 0 && $param <= 100; } ], 'page' => [ 'default' => 1, 'validate_callback' => function($param) { return is_numeric($param) && $param > 0; } ] ] ], [ 'methods' => 'POST', 'callback' => [__CLASS__, 'create_diagram'], 'permission_callback' => [__CLASS__, 'check_permission'] ] ]); register_rest_route('wpfd/v1', '/diagrams/(?P<id>d+)', [ [ 'methods' => 'GET', 'callback' => [__CLASS__, 'get_diagram'], 'permission_callback' => [__CLASS__, 'check_permission'] ], [ 'methods' => 'PUT', 'callback' => [__CLASS__, 'update_diagram'], 'permission_callback' => [__CLASS__, 'check_permission'] ], [ 'methods' => 'DELETE', 'callback' => [__CLASS__, 'delete_diagram'], 'permission_callback' => [__CLASS__, 'check_permission'] ] ]); // 导出接口 register_rest_route('wpfd/v1', '/export/(?P<id>d+)', [ [ 'methods' => 'GET', 'callback' => [__CLASS__, 'export_diagram'], 'permission_callback' => [__CLASS__, 'check_permission'], 'args' => [ 'format' => [ 'default' => 'png', 'validate_callback' => function($param) { return in_array($param, ['png', 'jpg', 'svg', 'pdf']); } ] ] ] ]); // 搜索接口 register_rest_route('wpfd/v1', '/search', [ [ 'methods' => 'GET', 'callback' => [__CLASS__, 'search_diagrams'], 'permission_callback' => [__CLASS__, 'check_permission'] ] ]); } public static function check_permission($request) { // 检查用户是否登录 if (!is_user_logged_in()) { return new WP_Error('rest_forbidden', __('请先登录'), ['status' => 401]); } // 检查用户权限 $method = $request->get_method(); $user_id = get_current_user_id(); // 对于GET请求,允许所有登录用户访问 if ($method === 'GET') { return true; } // 对于其他请求,需要编辑权限 if (!current_user_can('edit_posts')) { return new WP_Error('rest_forbidden', __('权限不足'), ['status' => 403]); } return true; } public static function get_diagrams($request) { $params = $request->get_params(); $per_page = intval($params['per_page']); $page = intval($params['page']); $offset = ($page - 1) * $per_page; global $wpdb; $table_name = $wpdb->prefix . 'wpfd_diagrams'; $user_id = get_current_user_id(); // 获取图表总数 $total = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table_name} WHERE user_id = %d", $user_id ) ); // 获取图表列表 $results = $wpdb->get_results( $wpdb->prepare( "SELECT id, title, diagram_type, created_at, updated_at FROM {$table_name} WHERE user_id = %d ORDER BY updated_at DESC LIMIT %d OFFSET %d", $user_id, $per_page, $offset ), ARRAY_A ); // 准备响应数据 $diagrams = []; foreach ($results as $row) { $diagrams[] = [ 'id' => intval($row['id']), 'title' => $row['title'], 'type' => $row['diagram_type'], 'created_at' => $row['created_at'], 'updated_at' => $row['updated_at'], 'edit_url' => admin_url('admin.php?page=wpfd-edit&id=' . $row['id']) ]; } $response = new WP_REST_Response($diagrams); $response->header('X-WP-Total', $total); $response->header('X-WP-TotalPages', ceil($total / $per_page)); return $response; } public static function get_diagram($request) { $id = intval($request['id']); $diagram = WPFD_Database::get_diagram($id); if (!$diagram) { return new WP_Error('not_found', __('图表不存在'), ['status' => 404]); } // 检查权限:只能访问自己的图表 if ($diagram['user_id'] != get_current_user_id() && !current_user_can('manage_options')) { return new WP_Error('forbidden', __('无权访问此图表'), ['status' => 403]); } return rest_ensure_response($diagram); } public static function create_diagram($request) { $data = $request->get_json_params(); // 验证数据 if (empty($data['title'])) { return new WP_Error('invalid_data', __('标题不能为空'), ['status' => 400]); } if (empty($data['diagram_data'])) { return new WP_Error('invalid_data', __('图表数据不能为空'), ['status' => 400]); } // 准备保存数据 $save_data = [ 'title' => sanitize_text_field($data['title']), 'diagram_data' => $data['diagram_data'], 'diagram_type' => isset($data['diagram_type']) ? sanitize_text_field($data['diagram_type']) : 'flowchart', 'settings' => isset($data['settings']) ? $data['settings'] : [] ]; // 如果有post_id,关联到文章 if (isset($data['post_id']) && $data['post_id

发表评论