WordPress开发教程:集成网站自动化社媒舆情监测与预警通知系统 引言:WordPress的无限可能 在当今数字化时代,企业网站已不再仅仅是展示信息的静态页面,而是需要具备智能化、自动化功能的综合平台。WordPress作为全球最受欢迎的内容管理系统,其真正的强大之处在于其高度的可扩展性和灵活性。通过代码二次开发,我们可以将WordPress从一个简单的博客平台转变为功能强大的业务工具。本教程将深入探讨如何通过WordPress程序开发,集成网站自动化社交媒体舆情监测与预警通知系统,同时实现常用互联网小工具功能,为您的网站增添智能化翅膀。 第一部分:WordPress开发环境搭建与准备 1.1 开发环境配置 在进行WordPress二次开发前,首先需要搭建合适的开发环境。推荐使用本地开发环境如XAMPP、MAMP或Local by Flywheel,这些工具可以快速搭建包含Apache、MySQL和PHP的完整环境。对于代码编辑器,Visual Studio Code或PHPStorm都是优秀的选择,它们提供了强大的代码提示、调试和版本控制集成功能。 确保您的开发环境满足以下要求: PHP版本7.4或更高(推荐8.0+) MySQL 5.6+或MariaDB 10.1+ Apache或Nginx服务器 启用必要的PHP扩展(curl、json、mbstring等) 1.2 子主题创建与结构规划 为避免直接修改主题文件导致更新时丢失更改,我们强烈建议创建子主题。在wp-content/themes目录下创建新文件夹,命名为您的主题名加“-child”,例如“twentytwentyone-child”。 子主题至少需要包含以下文件: style.css:包含主题元数据 functions.php:用于添加自定义功能 可选的模板文件 在style.css中添加: /* Theme Name: Twenty Twenty-One Child Template: twentytwentyone Version: 1.0 */ 1.3 必备开发工具与插件 安装以下开发辅助插件: Query Monitor:数据库查询和性能分析 Debug Bar:PHP错误和警告显示 Show Current Template:显示当前使用的模板文件 Advanced Custom Fields:自定义字段管理(可选但推荐) 第二部分:社交媒体舆情监测系统开发 2.1 系统架构设计 社交媒体舆情监测系统需要包含以下核心模块: 数据采集模块:从各社交媒体平台获取数据 数据处理模块:清洗、分析和分类数据 存储模块:将处理后的数据存入数据库 展示模块:在WordPress后台和前端展示数据 预警模块:根据设定规则触发通知 2.2 社交媒体API集成 首先,我们需要集成主流社交媒体的API。以下是一个基础类,用于处理多个平台的API连接: class SocialMediaMonitor { private $platforms = []; private $api_keys = []; public function __construct() { $this->init_platforms(); } private function init_platforms() { // 从数据库或配置文件中读取API密钥 $this->api_keys = get_option('social_media_api_keys', []); // 初始化各平台连接 $this->platforms = [ 'twitter' => new TwitterAPI($this->api_keys['twitter'] ?? ''), 'facebook' => new FacebookAPI($this->api_keys['facebook'] ?? ''), 'instagram' => new InstagramAPI($this->api_keys['instagram'] ?? ''), 'weibo' => new WeiboAPI($this->api_keys['weibo'] ?? ''), ]; } public function fetch_posts($platform, $keywords, $limit = 100) { if (!isset($this->platforms[$platform])) { return new WP_Error('invalid_platform', '不支持的社交媒体平台'); } try { return $this->platforms[$platform]->search($keywords, $limit); } catch (Exception $e) { error_log('社交媒体数据获取失败: ' . $e->getMessage()); return []; } } } 2.3 数据采集与定时任务 使用WordPress的Cron系统定时采集数据: class SocialMediaCrawler { public function __construct() { add_action('init', [$this, 'schedule_crawling']); add_action('social_media_crawl_hook', [$this, 'crawl_all_platforms']); } public function schedule_crawling() { if (!wp_next_scheduled('social_media_crawl_hook')) { wp_schedule_event(time(), 'hourly', 'social_media_crawl_hook'); } } public function crawl_all_platforms() { $monitor = new SocialMediaMonitor(); $keywords = get_option('monitoring_keywords', []); foreach (['twitter', 'facebook', 'instagram'] as $platform) { $posts = $monitor->fetch_posts($platform, $keywords, 50); $this->process_and_store($posts, $platform); } // 记录最后一次爬取时间 update_option('last_crawl_time', current_time('mysql')); } private function process_and_store($posts, $platform) { global $wpdb; $table_name = $wpdb->prefix . 'social_media_posts'; foreach ($posts as $post) { $data = [ 'platform' => $platform, 'post_id' => $post['id'], 'author' => sanitize_text_field($post['author']), 'content' => wp_kses_post($post['content']), 'url' => esc_url_raw($post['url']), 'likes' => intval($post['likes']), 'shares' => intval($post['shares']), 'comments' => intval($post['comments']), 'post_time' => $post['created_at'], 'sentiment' => $this->analyze_sentiment($post['content']), 'created_at' => current_time('mysql') ]; $wpdb->insert($table_name, $data); } } } 2.4 情感分析与关键词提取 集成自然语言处理功能,分析文本情感和提取关键词: class SentimentAnalyzer { private $positive_words = []; private $negative_words = []; public function __construct() { // 加载情感词典 $this->load_sentiment_dictionaries(); } public function analyze($text) { $words = $this->tokenize($text); $positive_score = 0; $negative_score = 0; foreach ($words as $word) { if (in_array($word, $this->positive_words)) { $positive_score++; } if (in_array($word, $this->negative_words)) { $negative_score++; } } $total = $positive_score + $negative_score; if ($total == 0) { return 'neutral'; } $score = ($positive_score - $negative_score) / $total; if ($score > 0.2) { return 'positive'; } elseif ($score < -0.2) { return 'negative'; } else { return 'neutral'; } } public function extract_keywords($text, $limit = 5) { // 使用TF-IDF算法或简单词频统计 $words = $this->tokenize($text); $stop_words = $this->get_stop_words(); // 过滤停用词 $filtered_words = array_diff($words, $stop_words); // 统计词频 $word_freq = array_count_values($filtered_words); // 按频率排序 arsort($word_freq); // 返回前N个关键词 return array_slice(array_keys($word_freq), 0, $limit); } } 第三部分:预警通知系统实现 3.1 预警规则配置系统 创建灵活可配置的预警规则系统: class AlertSystem { private $rules = []; public function __construct() { $this->load_rules(); add_action('new_social_media_post', [$this, 'check_alerts'], 10, 2); } private function load_rules() { $this->rules = get_option('alert_rules', [ [ 'id' => 1, 'name' => '负面舆情预警', 'conditions' => [ 'sentiment' => 'negative', 'engagement' => '>100', 'keywords' => ['投诉', '问题', '故障'] ], 'channels' => ['email', 'slack'], 'recipients' => ['admin@example.com'], 'cooldown' => 300 // 5分钟内不重复报警 ] ]); } public function check_alerts($post_data, $platform) { foreach ($this->rules as $rule) { if ($this->matches_rule($post_data, $rule)) { $this->trigger_alert($rule, $post_data); } } } private function matches_rule($post_data, $rule) { foreach ($rule['conditions'] as $key => $value) { if (!$this->check_condition($post_data, $key, $value)) { return false; } } return true; } private function trigger_alert($rule, $post_data) { $last_alert = get_transient('alert_' . $rule['id']); if ($last_alert) { return; // 还在冷却期内 } // 发送通知到各个渠道 foreach ($rule['channels'] as $channel) { switch ($channel) { case 'email': $this->send_email_alert($rule, $post_data); break; case 'slack': $this->send_slack_alert($rule, $post_data); break; case 'webhook': $this->send_webhook_alert($rule, $post_data); break; } } // 设置冷却期 set_transient('alert_' . $rule['id'], true, $rule['cooldown']); // 记录报警历史 $this->log_alert($rule, $post_data); } } 3.2 多渠道通知集成 实现多种通知渠道: class NotificationChannels { public function send_email($to, $subject, $message, $headers = '') { if (empty($headers)) { $headers = [ 'Content-Type: text/html; charset=UTF-8', 'From: 舆情监测系统 <alerts@yourdomain.com>' ]; } return wp_mail($to, $subject, $message, $headers); } public function send_slack($webhook_url, $message, $channel = '#alerts') { $payload = [ 'channel' => $channel, 'username' => '舆情监测机器人', 'text' => $message, 'icon_emoji' => ':warning:' ]; $args = [ 'body' => json_encode($payload), 'headers' => ['Content-Type' => 'application/json'], 'timeout' => 30 ]; return wp_remote_post($webhook_url, $args); } public function send_webhook($url, $data) { $args = [ 'body' => json_encode($data), 'headers' => ['Content-Type' => 'application/json'], 'timeout' => 30 ]; return wp_remote_post($url, $args); } public function send_sms($phone, $message) { // 集成短信服务商API $api_key = get_option('sms_api_key'); $api_url = 'https://api.sms-provider.com/send'; $data = [ 'apikey' => $api_key, 'mobile' => $phone, 'text' => $message ]; return wp_remote_post($api_url, [ 'body' => $data, 'timeout' => 30 ]); } } 3.3 实时仪表盘与数据可视化 创建实时监控仪表盘: class MonitoringDashboard { public function __construct() { add_action('admin_menu', [$this, 'add_admin_pages']); add_action('wp_dashboard_setup', [$this, 'add_dashboard_widget']); add_shortcode('social_media_monitor', [$this, 'shortcode_display']); } public function add_admin_pages() { add_menu_page( '舆情监测系统', '舆情监测', 'manage_options', 'social-media-monitor', [$this, 'render_dashboard'], 'dashicons-chart-line', 30 ); // 添加子菜单 add_submenu_page( 'social-media-monitor', '预警设置', '预警设置', 'manage_options', 'alert-settings', [$this, 'render_alert_settings'] ); } public function render_dashboard() { ?> <div class="wrap"> <h1>社交媒体舆情监测仪表盘</h1> <div class="dashboard-grid"> <div class="dashboard-card"> <h3>今日舆情概览</h3> <div id="sentiment-chart"></div> </div> <div class="dashboard-card"> <h3>平台分布</h3> <div id="platform-chart"></div> </div> <div class="dashboard-card full-width"> <h3>实时动态</h3> <div id="realtime-feed"></div> </div> </div> <script> // 使用Chart.js或ECharts渲染图表 </script> </div> <?php } public function add_dashboard_widget() { wp_add_dashboard_widget( 'social_media_widget', '社交媒体舆情监控', [$this, 'render_dashboard_widget'] ); } public function shortcode_display($atts) { $atts = shortcode_atts([ 'platform' => 'all', 'limit' => 10, 'show_sentiment' => true ], $atts); ob_start(); $this->render_public_display($atts); return ob_get_clean(); } } 第四部分:常用互联网小工具集成 4.1 多功能工具类开发 创建通用工具类,集成常用功能: class WordPressTools { // URL缩短功能 public static function shorten_url($url, $service = 'bitly') { $api_keys = get_option('url_shortener_keys', []); switch ($service) { case 'bitly': return $this->bitly_shorten($url, $api_keys['bitly'] ?? ''); case 'tinyurl': return $this->tinyurl_shorten($url); default: return $url; } } // 二维码生成 public static function generate_qrcode($data, $size = 200) { $api_url = 'https://api.qrserver.com/v1/create-qr-code/'; return add_query_arg([ 'data' => urlencode($data), 'size' => $size . 'x' . $size ], $api_url); } // 内容摘要生成 public static function generate_excerpt($content, $length = 200) { $content = strip_tags($content); $content = preg_replace('/s+/', ' ', $content); if (mb_strlen($content) <= $length) { return $content; } return mb_substr($content, 0, $length) . '...'; } // 图片压缩与优化 public static function optimize_image($image_url, $quality = 80) { // 使用WordPress图像处理API $upload_dir = wp_upload_dir(); $image_path = str_replace($upload_dir['baseurl'], $upload_dir['basedir'], $image_url); if (file_exists($image_path)) { $editor = wp_get_image_editor($image_path); if (!is_wp_error($editor)) { $editor->set_quality($quality); $result = $editor->save(); if (!is_wp_error($result)) { return str_replace($upload_dir['basedir'], $upload_dir['baseurl'], $result['path']); } } } return $image_url; } } 4.2 短代码与小工具集成 创建易于使用的短代码和小工具: class ToolShortcodes { public function __construct() { // 注册短代码 add_shortcode('qrcode', [$this, 'qrcode_shortcode']); add_shortcode('countdown', [$this, 'countdown_shortcode']); add_shortcode('weather', [$this, 'weather_shortcode']); add_shortcode('calculator', [$this, 'calculator_shortcode']); // 注册小工具 add_action('widgets_init', [$this, 'register_widgets']); } public function qrcode_shortcode($atts) { $atts = shortcode_atts([ 'data' => get_permalink(), 'size' => '150', 'color' => '000000', 'bgcolor' => 'ffffff' ], $atts); $url = 'https://api.qrserver.com/v1/create-qr-code/'; $params = [ 'data' => $atts['data'], 'size' => $atts['size'] . 'x' . $atts['size'], 'color' => $atts['color'], 'bgcolor' => $atts['bgcolor'] ]; $src = add_query_arg($params, $url); return sprintf( 4.3 实用小工具示例:实时天气与计算器 // 天气小工具类 class WeatherWidget extends WP_Widget { public function __construct() { parent::__construct( 'weather_widget', '实时天气', ['description' => '显示当前位置的实时天气信息'] ); } public function widget($args, $instance) { echo $args['before_widget']; if (!empty($instance['title'])) { echo $args['before_title'] . apply_filters('widget_title', $instance['title']) . $args['after_title']; } $location = !empty($instance['location']) ? $instance['location'] : 'auto'; $units = !empty($instance['units']) ? $instance['units'] : 'metric'; $weather_data = $this->get_weather_data($location, $units); if ($weather_data) { ?> <div class="weather-widget"> <div class="weather-current"> <div class="weather-icon"> <img src="<?php echo esc_url($weather_data['icon']); ?>" alt="<?php echo esc_attr($weather_data['description']); ?>"> </div> <div class="weather-temp"> <span class="temp-number"><?php echo round($weather_data['temp']); ?></span> <span class="temp-unit">°<?php echo $units === 'metric' ? 'C' : 'F'; ?></span> </div> <div class="weather-details"> <p class="weather-desc"><?php echo esc_html($weather_data['description']); ?></p> <p class="weather-location"><?php echo esc_html($weather_data['location']); ?></p> <div class="weather-extra"> <span>湿度: <?php echo $weather_data['humidity']; ?>%</span> <span>风速: <?php echo $weather_data['wind_speed']; ?> km/h</span> </div> </div> </div> <?php if (!empty($weather_data['forecast'])) : ?> <div class="weather-forecast"> <?php foreach (array_slice($weather_data['forecast'], 0, 5) as $day) : ?> <div class="forecast-day"> <span class="day-name"><?php echo $day['day']; ?></span> <img src="<?php echo esc_url($day['icon']); ?>" alt="<?php echo esc_attr($day['desc']); ?>"> <span class="day-temp"><?php echo round($day['temp_max']); ?>°</span> </div> <?php endforeach; ?> </div> <?php endif; ?> </div> <?php } else { echo '<p>无法获取天气数据</p>'; } echo $args['after_widget']; } private function get_weather_data($location, $units) { $api_key = get_option('weather_api_key', ''); $cache_key = 'weather_data_' . md5($location . $units); $cached_data = get_transient($cache_key); if ($cached_data !== false) { return $cached_data; } if (empty($api_key)) { return false; } $api_url = 'https://api.openweathermap.org/data/2.5/weather'; if ($location === 'auto') { // 尝试获取用户IP位置 $ip = $_SERVER['REMOTE_ADDR']; $geo_data = wp_remote_get("http://ip-api.com/json/{$ip}"); if (!is_wp_error($geo_data)) { $geo = json_decode(wp_remote_retrieve_body($geo_data), true); if ($geo && $geo['status'] === 'success') { $location = "{$geo['lat']},{$geo['lon']}"; } } } $params = [ 'q' => $location, 'appid' => $api_key, 'units' => $units, 'lang' => 'zh_cn' ]; $response = wp_remote_get(add_query_arg($params, $api_url)); if (is_wp_error($response)) { return false; } $data = json_decode(wp_remote_retrieve_body($response), true); if ($data && $data['cod'] == 200) { $weather_data = [ 'location' => $data['name'] . ', ' . $data['sys']['country'], 'temp' => $data['main']['temp'], 'humidity' => $data['main']['humidity'], 'pressure' => $data['main']['pressure'], 'wind_speed' => $data['wind']['speed'], 'description' => $data['weather'][0]['description'], 'icon' => "https://openweathermap.org/img/wn/{$data['weather'][0]['icon']}@2x.png", 'forecast' => $this->get_forecast($data['coord']['lat'], $data['coord']['lon'], $api_key, $units) ]; // 缓存1小时 set_transient($cache_key, $weather_data, HOUR_IN_SECONDS); return $weather_data; } return false; } } // 计算器短代码实现 public function calculator_shortcode($atts) { $atts = shortcode_atts([ 'type' => 'basic', 'theme' => 'light', 'currency' => 'CNY' ], $atts); ob_start(); ?> <div class="calculator-container" data-theme="<?php echo esc_attr($atts['theme']); ?>"> <?php if ($atts['type'] === 'basic') : ?> <div class="calculator basic-calculator"> <div class="calculator-display"> <input type="text" readonly value="0" class="calc-display"> </div> <div class="calculator-buttons"> <button class="calc-btn operator" data-action="clear">C</button> <button class="calc-btn operator" data-action="backspace">⌫</button> <button class="calc-btn operator" data-action="percentage">%</button> <button class="calc-btn operator" data-action="divide">÷</button> <button class="calc-btn number" data-number="7">7</button> <button class="calc-btn number" data-number="8">8</button> <button class="calc-btn number" data-number="9">9</button> <button class="calc-btn operator" data-action="multiply">×</button> <button class="calc-btn number" data-number="4">4</button> <button class="calc-btn number" data-number="5">5</button> <button class="calc-btn number" data-number="6">6</button> <button class="calc-btn operator" data-action="subtract">-</button> <button class="calc-btn number" data-number="1">1</button> <button class="calc-btn number" data-number="2">2</button> <button class="calc-btn number" data-number="3">3</button> <button class="calc-btn operator" data-action="add">+</button> <button class="calc-btn number zero" data-number="0">0</button> <button class="calc-btn number" data-number=".">.</button> <button class="calc-btn operator equals" data-action="equals">=</button> </div> </div> <?php elseif ($atts['type'] === 'currency') : ?> <div class="calculator currency-converter"> <div class="converter-row"> <input type="number" class="amount-input" value="1" min="0" step="0.01"> <select class="currency-select from-currency"> <option value="CNY">人民币 (CNY)</option> <option value="USD">美元 (USD)</option> <option value="EUR">欧元 (EUR)</option> <option value="JPY">日元 (JPY)</option> <option value="GBP">英镑 (GBP)</option> </select> </div> <div class="converter-swap"> <button class="swap-btn">⇅</button> </div> <div class="converter-row"> <input type="number" class="amount-output" readonly> <select class="currency-select to-currency"> <option value="USD">美元 (USD)</option> <option value="CNY">人民币 (CNY)</option> <option value="EUR">欧元 (EUR)</option> <option value="JPY">日元 (JPY)</option> <option value="GBP">英镑 (GBP)</option> </select> </div> <div class="converter-rate"> 汇率: <span class="rate-value">1 CNY = 0.14 USD</span> <span class="rate-update">更新时间: <span class="update-time"></span></span> </div> </div> <?php endif; ?> </div> <script> (function($) { 'use strict'; <?php if ($atts['type'] === 'basic') : ?> // 基础计算器逻辑 $(document).ready(function() { let currentInput = '0'; let previousInput = ''; let operation = null; let resetScreen = false; $('.basic-calculator .calc-display').val(currentInput); $('.calc-btn.number').on('click', function() { const number = $(this).data('number'); if (currentInput === '0' || resetScreen) { currentInput = number; resetScreen = false; } else { currentInput += number; } $('.calc-display').val(currentInput); }); $('.calc-btn.operator').on('click', function() { const action = $(this).data('action'); switch(action) { case 'clear': currentInput = '0'; previousInput = ''; operation = null; break; case 'backspace': if (currentInput.length > 1) { currentInput = currentInput.slice(0, -1); } else { currentInput = '0'; } break; case 'percentage': currentInput = (parseFloat(currentInput) / 100).toString(); break; case 'add': case 'subtract': case 'multiply': case 'divide': if (previousInput !== '') { calculate(); } operation = action; previousInput = currentInput; resetScreen = true; break; case 'equals': if (previousInput !== '' && operation !== null) { calculate(); operation = null; previousInput = ''; } break; } $('.calc-display').val(currentInput); }); function calculate() { let prev = parseFloat(previousInput); let current = parseFloat(currentInput); let result = 0; switch(operation) { case 'add': result = prev + current; break; case 'subtract': result = prev - current; break; case 'multiply': result = prev * current; break; case 'divide': result = current !== 0 ? prev / current : '错误'; break; } currentInput = result.toString(); resetScreen = true; } }); <?php elseif ($atts['type'] === 'currency') : ?> // 货币转换器逻辑 $(document).ready(function() { const apiKey = '<?php echo get_option('currency_api_key', ''); ?>'; let exchangeRates = {}; let lastUpdate = null; function updateExchangeRates() { if (!apiKey) { // 使用免费API $.getJSON('https://api.exchangerate-api.com/v4/latest/CNY') .done(function(data) { exchangeRates = data.rates; lastUpdate = new Date(data.date); updateDisplay(); }) .fail(function() { // 备用数据源 loadFallbackRates(); }); } else { // 使用付费API $.ajax({ url: 'https://api.currencyapi.com/v3/latest', headers: { 'apikey': apiKey }, success: function(data) { exchangeRates = data.data; lastUpdate = new Date(data.meta.last_updated_at); updateDisplay(); } }); } } function loadFallbackRates() { // 硬编码的汇率(示例数据) exchangeRates = { 'CNY': 1, 'USD': 0.14, 'EUR': 0.13, 'JPY': 15.5, 'GBP': 0.11 }; lastUpdate = new Date(); updateDisplay(); } function updateDisplay() { const fromCurrency = $('.from-currency').val(); const toCurrency = $('.to-currency').val(); const amount = parseFloat($('.amount-input').val()); if (exchangeRates[fromCurrency] && exchangeRates[toCurrency]) { const rate = exchangeRates[toCurrency] / exchangeRates[fromCurrency]; const converted = amount * rate; $('.amount-output').val(converted.toFixed(2)); $('.rate-value').text(`1 ${fromCurrency} = ${rate.toFixed(4)} ${toCurrency}`); $('.update-time').text(lastUpdate.toLocaleTimeString()); } } // 初始化 updateExchangeRates(); // 事件监听 $('.amount-input, .from-currency, .to-currency').on('input change', updateDisplay); $('.swap-btn').on('click', function() { const fromVal = $('.from-currency').val(); const toVal = $('.to-currency').val(); $('.from-currency').val(toVal); $('.to-currency').val(fromVal); updateDisplay(); }); // 每30分钟更新一次汇率 setInterval(updateExchangeRates, 30 * 60 * 1000); }); <?php endif; ?> })(jQuery); </script> <style> .calculator-container { max-width: 400px; margin: 20px auto; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 12px rgba(0,0,0,0.1); } .basic-calculator { background: #f5f5f5; padding: 20px; } .calculator-display { margin-bottom: 20px; } .calc-display { width: 100%; height: 60px; font-size: 24px; text-align: right; padding: 10px; border: none; background: white; border-radius: 5px; box-shadow: inset 0 2px 5px rgba(0,0,0,0.1); } .calculator-buttons { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; } .calc-btn { height: 50px; font-size: 18px; border: none; border-radius: 5px; cursor: pointer; transition: all 0.2s; } .calc-btn.number { background: white; color: #333; } .calc-btn.operator { background: #4a90e2; color: white; } .calc-btn.equals { background: #f39c12; color: white; } .calc-btn.zero { grid-column: span 2; } .calc-btn:hover { opacity: 0.9; transform: translateY(-2px); } .currency-converter { background: white; padding: 20px; } .converter-row { display: flex; gap: 10px; margin-bottom: 15px; } .amount-input, .amount-output { flex: 1; height: 50px; font-size: 18px; padding: 10px; border: 1px solid #ddd; border-radius: 5px; } .currency-select { width: 150px; height: 50px; font-size: 16px; padding: 10px; border: 1px solid #ddd; border-radius: 5px; } .converter-swap { text-align: center; margin: 10px 0; } .swap-btn { width: 40px; height: 40px; border-radius: 50%; border: 2px solid #4a90e2; background: white; color: #4a90e2; font-size: 18px; cursor: pointer; transition: all 0.2s; } .swap-btn:hover { background: #4a90e2; color: white; } .converter-rate { margin-top: 20px; padding-top: 15px; border-top: 1px solid #eee; font-size: 14px; color: #666; } .rate-value { font-weight: bold; color: #333; } .rate-update { float: right; } </style> <?php return ob_get_clean(); } 第五部分:系统优化与安全加固 5.1 性能优化策略
发表评论分类: 应用软件
实战教学:为你的网站添加轻量级互动小游戏以提升用户参与度 引言:为什么网站需要互动小游戏? 在当今信息爆炸的互联网时代,用户注意力已成为最宝贵的资源。网站运营者面临着一个共同的挑战:如何在众多竞争者中脱颖而出,吸引并留住访问者?答案可能比你想象的更简单——互动性。 研究表明,具有互动元素的网站比静态网站的用户停留时间高出40%以上,页面浏览量增加35%,用户回访率提升25%。轻量级小游戏正是实现这种互动性的绝佳方式,它们不仅能够提升用户参与度,还能增强品牌记忆点,促进内容分享,甚至直接转化为商业价值。 WordPress作为全球最流行的内容管理系统,其强大的可扩展性为我们提供了实现这一目标的完美平台。通过代码二次开发,我们可以在不显著影响网站性能的前提下,为网站注入活力与趣味性。 第一部分:准备工作与环境搭建 1.1 选择合适的开发环境 在开始之前,我们需要确保拥有一个适合WordPress开发的环境: 本地开发环境:推荐使用XAMPP、MAMP或Local by Flywheel WordPress安装:最新稳定版本(建议5.8+) 代码编辑器:VS Code、Sublime Text或PHPStorm 浏览器开发者工具:Chrome DevTools或Firefox Developer Edition 1.2 创建自定义插件框架 为了避免主题更新时丢失自定义功能,我们将创建一个独立的插件: <?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('LIGHTGAMES_VERSION', '1.0.0'); define('LIGHTGAMES_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('LIGHTGAMES_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 function lightgames_init() { // 加载文本域(用于国际化) load_plugin_textdomain('lightgames', false, dirname(plugin_basename(__FILE__)) . '/languages'); // 注册短代码 add_shortcode('lightgame_quiz', 'lightgames_quiz_shortcode'); add_shortcode('lightgame_memory', 'lightgames_memory_shortcode'); add_shortcode('lightgame_spinwheel', 'lightgames_spinwheel_shortcode'); // 注册管理菜单 if (is_admin()) { add_action('admin_menu', 'lightgames_admin_menu'); } } add_action('init', 'lightgames_init'); // 添加管理菜单 function lightgames_admin_menu() { add_menu_page( '互动游戏设置', '互动游戏', 'manage_options', 'lightgames-settings', 'lightgames_settings_page', 'dashicons-games', 30 ); } ?> 第二部分:实现第一个小游戏——知识问答 2.1 设计游戏逻辑与数据结构 知识问答是最简单且有效的互动形式之一。我们将创建一个可配置的问答游戏: // 在插件主文件中添加以下函数 function lightgames_quiz_shortcode($atts) { // 解析短代码属性 $atts = shortcode_atts(array( 'category' => 'general', 'questions' => 5, 'time' => 30 ), $atts, 'lightgame_quiz'); // 生成唯一ID用于游戏实例 $quiz_id = 'quiz_' . uniqid(); // 获取问题数据(实际应用中应从数据库或配置中获取) $questions = lightgames_get_quiz_questions($atts['category'], $atts['questions']); // 输出游戏HTML结构 ob_start(); ?> <div class="lightgame-quiz-container" id="<?php echo esc_attr($quiz_id); ?>" data-time="<?php echo esc_attr($atts['time']); ?>"> <div class="quiz-header"> <h3>知识挑战赛</h3> <div class="quiz-stats"> <span class="score">得分: <strong>0</strong></span> <span class="timer">时间: <strong><?php echo esc_html($atts['time']); ?></strong>秒</span> <span class="progress">问题: <strong>1</strong>/<?php echo esc_html($atts['questions']); ?></span> </div> </div> <div class="quiz-content"> <div class="question-container"> <!-- 问题将通过JavaScript动态加载 --> </div> <div class="options-container"> <!-- 选项将通过JavaScript动态加载 --> </div> <div class="quiz-controls"> <button class="quiz-btn prev-btn" disabled>上一题</button> <button class="quiz-btn next-btn">下一题</button> <button class="quiz-btn submit-btn">提交答案</button> </div> </div> <div class="quiz-results" style="display:none;"> <h3>测验完成!</h3> <div class="final-score">你的得分: <span>0</span>/<?php echo esc_html($atts['questions']); ?></div> <div class="result-message"></div> <button class="quiz-btn restart-btn">再试一次</button> <button class="quiz-btn share-btn">分享结果</button> </div> </div> <script type="application/json" class="quiz-questions-data"> <?php echo wp_json_encode($questions); ?> </script> <?php return ob_get_clean(); } // 获取问题数据的辅助函数 function lightgames_get_quiz_questions($category, $limit) { // 这里应该是从数据库或配置文件中获取问题 // 为示例目的,我们返回静态数据 $questions = array( array( 'question' => 'WordPress最初是什么类型的平台?', 'options' => array('博客平台', '电商系统', '社交网络', '论坛软件'), 'correct' => 0, 'explanation' => 'WordPress最初于2003年作为一个博客平台发布。' ), array( 'question' => '以下哪个不是JavaScript框架?', 'options' => array('React', 'Vue', 'Laravel', 'Angular'), 'correct' => 2, 'explanation' => 'Laravel是PHP框架,不是JavaScript框架。' ), // 可以添加更多问题... ); // 随机选择指定数量的问题 shuffle($questions); return array_slice($questions, 0, min($limit, count($questions))); } 2.2 添加游戏样式与交互逻辑 创建CSS和JavaScript文件来实现游戏的视觉表现和交互: /* lightgames.css */ .lightgame-quiz-container { max-width: 800px; margin: 20px auto; border: 1px solid #e0e0e0; border-radius: 10px; overflow: hidden; box-shadow: 0 5px 15px rgba(0,0,0,0.05); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; } .quiz-header { background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%); color: white; padding: 20px; text-align: center; } .quiz-header h3 { margin: 0 0 15px 0; font-size: 1.8em; } .quiz-stats { display: flex; justify-content: space-around; font-size: 1.1em; } .quiz-stats strong { font-weight: 700; color: #ffde59; } .quiz-content { padding: 25px; background: #f9f9f9; } .question-container { margin-bottom: 25px; } .question-container h4 { font-size: 1.4em; color: #333; margin-bottom: 15px; line-height: 1.4; } .options-container { display: grid; grid-template-columns: 1fr; gap: 12px; margin-bottom: 25px; } @media (min-width: 600px) { .options-container { grid-template-columns: 1fr 1fr; } } .quiz-option { padding: 15px; background: white; border: 2px solid #e0e0e0; border-radius: 8px; cursor: pointer; transition: all 0.2s ease; font-size: 1.1em; text-align: left; } .quiz-option:hover { border-color: #2575fc; background: #f0f7ff; } .quiz-option.selected { border-color: #4CAF50; background: #e8f5e9; } .quiz-option.correct { border-color: #4CAF50; background: #e8f5e9; } .quiz-option.incorrect { border-color: #f44336; background: #ffebee; } .quiz-controls { display: flex; justify-content: space-between; margin-top: 20px; } .quiz-btn { padding: 12px 25px; background: #2575fc; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 1em; font-weight: 600; transition: all 0.2s ease; } .quiz-btn:hover { background: #1c65e0; transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.1); } .quiz-btn:disabled { background: #cccccc; cursor: not-allowed; transform: none; box-shadow: none; } .quiz-results { padding: 30px; text-align: center; background: white; } .final-score { font-size: 2em; margin: 20px 0; color: #333; } .final-score span { color: #4CAF50; font-weight: 700; } .result-message { font-size: 1.2em; margin: 20px 0; padding: 15px; background: #f5f5f5; border-radius: 8px; } // lightgames.js document.addEventListener('DOMContentLoaded', function() { // 初始化所有问答游戏 document.querySelectorAll('.lightgame-quiz-container').forEach(initQuizGame); }); function initQuizGame(quizContainer) { const questionsData = JSON.parse(quizContainer.querySelector('.quiz-questions-data').textContent); const timeLimit = parseInt(quizContainer.getAttribute('data-time')) || 30; let currentQuestion = 0; let score = 0; let userAnswers = []; let timeLeft = timeLimit; let timerInterval; const questionEl = quizContainer.querySelector('.question-container'); const optionsEl = quizContainer.querySelector('.options-container'); const scoreEl = quizContainer.querySelector('.score strong'); const timerEl = quizContainer.querySelector('.timer strong'); const progressEl = quizContainer.querySelector('.progress strong'); const prevBtn = quizContainer.querySelector('.prev-btn'); const nextBtn = quizContainer.querySelector('.next-btn'); const submitBtn = quizContainer.querySelector('.submit-btn'); const resultsEl = quizContainer.querySelector('.quiz-results'); const finalScoreEl = quizContainer.querySelector('.final-score span'); const resultMessageEl = quizContainer.querySelector('.result-message'); const restartBtn = quizContainer.querySelector('.restart-btn'); const shareBtn = quizContainer.querySelector('.share-btn'); // 初始化游戏 function initGame() { loadQuestion(currentQuestion); startTimer(); updateControls(); } // 加载问题 function loadQuestion(index) { const question = questionsData[index]; // 更新问题文本 questionEl.innerHTML = `<h4>${index + 1}. ${question.question}</h4>`; // 清空选项容器 optionsEl.innerHTML = ''; // 添加选项 question.options.forEach((option, i) => { const optionEl = document.createElement('button'); optionEl.className = 'quiz-option'; if (userAnswers[index] === i) { optionEl.classList.add('selected'); } optionEl.textContent = option; optionEl.addEventListener('click', () => selectOption(i)); optionsEl.appendChild(optionEl); }); // 更新进度 progressEl.textContent = index + 1; } // 选择选项 function selectOption(optionIndex) { // 移除之前的选择 optionsEl.querySelectorAll('.quiz-option').forEach(opt => { opt.classList.remove('selected'); }); // 标记当前选择 optionsEl.children[optionIndex].classList.add('selected'); // 保存答案 userAnswers[currentQuestion] = optionIndex; // 更新控制按钮状态 updateControls(); } // 更新控制按钮状态 function updateControls() { prevBtn.disabled = currentQuestion === 0; nextBtn.disabled = currentQuestion === questionsData.length - 1; submitBtn.disabled = userAnswers[currentQuestion] === undefined; } // 开始计时器 function startTimer() { clearInterval(timerInterval); timeLeft = timeLimit; updateTimerDisplay(); timerInterval = setInterval(() => { timeLeft--; updateTimerDisplay(); if (timeLeft <= 0) { clearInterval(timerInterval); endGame(); } }, 1000); } // 更新计时器显示 function updateTimerDisplay() { timerEl.textContent = timeLeft; // 时间不足时改变颜色 if (timeLeft <= 10) { timerEl.style.color = '#f44336'; } else { timerEl.style.color = ''; } } // 下一题 nextBtn.addEventListener('click', () => { if (currentQuestion < questionsData.length - 1) { currentQuestion++; loadQuestion(currentQuestion); updateControls(); } }); // 上一题 prevBtn.addEventListener('click', () => { if (currentQuestion > 0) { currentQuestion--; loadQuestion(currentQuestion); updateControls(); } }); // 提交答案 submitBtn.addEventListener('click', endGame); // 结束游戏 function endGame() { clearInterval(timerInterval); // 计算得分 score = 0; questionsData.forEach((question, index) => { if (userAnswers[index] === question.correct) { score++; } }); // 显示结果 quizContainer.querySelector('.quiz-content').style.display = 'none'; resultsEl.style.display = 'block'; finalScoreEl.textContent = `${score}/${questionsData.length}`; // 根据得分显示不同消息 const percentage = (score / questionsData.length) * 100; let message = ''; if (percentage >= 90) { message = '太棒了!你简直是专家!'; } else if (percentage >= 70) { message = '做得不错!你对这个主题有很好的了解。'; } else if (percentage >= 50) { message = '还可以,但还有提升空间。'; } else { message = '可能需要再复习一下这个主题。'; } resultMessageEl.textContent = message; // 更新得分显示 scoreEl.textContent = score; } // 重新开始 restartBtn.addEventListener('click', () => { currentQuestion = 0; score = 0; userAnswers = []; timeLeft = timeLimit; quizContainer.querySelector('.quiz-content').style.display = 'block'; resultsEl.style.display = 'none'; initGame(); }); // 分享结果 shareBtn.addEventListener('click', () => { const shareText = `我在知识挑战中获得了${score}/${questionsData.length}分!你也来试试吧!`; if (navigator.share) { navigator.share({ title: '知识挑战赛结果', text: shareText, url: window.location.href }); } else { // 备用方案:复制到剪贴板 navigator.clipboard.writeText(shareText + ' ' + window.location.href) .then(() => alert('结果已复制到剪贴板!')); } }); // 开始游戏 initGame(); } 2.3 注册并加载资源文件 在插件初始化函数中添加资源加载代码: // 在lightgames_init函数中添加 function lightgames_init() { // ... 之前的代码 ... // 注册前端资源 add_action('wp_enqueue_scripts', 'lightgames_enqueue_scripts'); } // 加载前端资源 function lightgames_enqueue_scripts() { // 仅在有短代码的页面加载资源 global $post; if (is_a($post, 'WP_Post') && (has_shortcode($post->post_content, 'lightgame_quiz') || has_shortcode($post->post_content, 'lightgame_memory') || has_shortcode($post->post_content, 'lightgame_spinwheel'))) { // 加载CSS wp_enqueue_style( 'lightgames-style', LIGHTGAMES_PLUGIN_URL . 'assets/css/lightgames.css', array(), LIGHTGAMES_VERSION ); 第三部分:实现第二个小游戏——记忆匹配 3.1 设计记忆匹配游戏逻辑 记忆匹配游戏是经典的互动游戏,能够有效提升用户的记忆力和参与度。我们将创建一个可配置的记忆卡片匹配游戏: // 在插件主文件中添加记忆游戏短代码函数 function lightgames_memory_shortcode($atts) { // 解析短代码属性 $atts = shortcode_atts(array( 'pairs' => 8, 'theme' => 'animals', 'difficulty' => 'medium' ), $atts, 'lightgame_memory'); // 根据难度设置时间限制 $time_limits = array( 'easy' => 120, 'medium' => 90, 'hard' => 60 ); $time_limit = isset($time_limits[$atts['difficulty']]) ? $time_limits[$atts['difficulty']] : 90; // 生成唯一ID $game_id = 'memory_' . uniqid(); // 获取卡片数据 $cards = lightgames_get_memory_cards($atts['pairs'], $atts['theme']); ob_start(); ?> <div class="lightgame-memory-container" id="<?php echo esc_attr($game_id); ?>" data-pairs="<?php echo esc_attr($atts['pairs']); ?>" data-time="<?php echo esc_attr($time_limit); ?>"> <div class="memory-header"> <h3>记忆大挑战</h3> <div class="memory-stats"> <span class="moves">步数: <strong>0</strong></span> <span class="timer">时间: <strong><?php echo esc_html($time_limit); ?></strong>秒</span> <span class="matches">匹配: <strong>0</strong>/<?php echo esc_html($atts['pairs']); ?></span> </div> <div class="difficulty-badge difficulty-<?php echo esc_attr($atts['difficulty']); ?>"> <?php echo esc_html(ucfirst($atts['difficulty'])); ?> 难度 </div> </div> <div class="memory-grid-container"> <div class="memory-grid" style="grid-template-columns: repeat(<?php echo min(6, ceil(sqrt($atts['pairs'] * 2))); ?>, 1fr);"> <!-- 卡片将通过JavaScript动态生成 --> </div> </div> <div class="memory-controls"> <button class="memory-btn start-btn">开始游戏</button> <button class="memory-btn restart-btn" disabled>重新开始</button> <button class="memory-btn hint-btn" disabled>提示</button> </div> <div class="memory-results" style="display:none;"> <h3>游戏完成!</h3> <div class="stats-summary"> <div class="stat-item"> <span class="stat-label">用时:</span> <span class="stat-value time-used">0</span>秒 </div> <div class="stat-item"> <span class="stat-label">步数:</span> <span class="stat-value total-moves">0</span> </div> <div class="stat-item"> <span class="stat-label">准确率:</span> <span class="stat-value accuracy">100%</span> </div> <div class="stat-item"> <span class="stat-label">得分:</span> <span class="stat-value final-score">0</span> </div> </div> <div class="performance-rating"></div> <div class="results-controls"> <button class="memory-btn play-again-btn">再玩一次</button> <button class="memory-btn share-results-btn">分享成绩</button> </div> </div> </div> <script type="application/json" class="memory-cards-data"> <?php echo wp_json_encode($cards); ?> </script> <?php return ob_get_clean(); } // 获取记忆卡片数据的辅助函数 function lightgames_get_memory_cards($pairs, $theme) { $pairs = min($pairs, 12); // 限制最大对数 // 不同主题的图标 $themes = array( 'animals' => array('🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼', '🐨', '🐯', '🦁', '🐮'), 'food' => array('🍎', '🍌', '🍇', '🍓', '🍒', '🍑', '🍍', '🥭', '🍉', '🍊', '🍋', '🥝'), 'sports' => array('⚽', '🏀', '🏈', '⚾', '🎾', '🏐', '🏉', '🎱', '🏓', '🏸', '🏒', '🏏'), 'flags' => array('🇨🇳', '🇺🇸', '🇬🇧', '🇯🇵', '🇰🇷', '🇫🇷', '🇩🇪', '🇮🇹', '🇪🇸', '🇷🇺', '🇧🇷', '🇦🇺'), 'vehicles' => array('🚗', '🚕', '🚙', '🚌', '🚎', '🏎️', '🚓', '🚑', '🚒', '🚐', '🚚', '🚛') ); // 默认使用动物主题 $icons = isset($themes[$theme]) ? $themes[$theme] : $themes['animals']; // 创建卡片对 $cards = array(); for ($i = 0; $i < $pairs; $i++) { $icon = $icons[$i % count($icons)]; $cards[] = array( 'id' => $i * 2, 'value' => $icon, 'matched' => false ); $cards[] = array( 'id' => $i * 2 + 1, 'value' => $icon, 'matched' => false ); } // 打乱顺序 shuffle($cards); return $cards; } 3.2 添加记忆游戏样式 在lightgames.css文件中添加记忆游戏的样式: /* 记忆匹配游戏样式 */ .lightgame-memory-container { max-width: 900px; margin: 30px auto; background: #fff; border-radius: 15px; overflow: hidden; box-shadow: 0 10px 30px rgba(0,0,0,0.1); } .memory-header { background: linear-gradient(135deg, #ff6b6b 0%, #ffa726 100%); color: white; padding: 20px; position: relative; } .memory-header h3 { margin: 0 0 15px 0; font-size: 1.8em; text-align: center; } .memory-stats { display: flex; justify-content: space-around; font-size: 1.1em; margin-bottom: 10px; } .memory-stats strong { font-weight: 700; color: #fffacd; } .difficulty-badge { position: absolute; top: 20px; right: 20px; padding: 5px 15px; border-radius: 20px; font-size: 0.9em; font-weight: 600; } .difficulty-easy { background: #4CAF50; color: white; } .difficulty-medium { background: #FF9800; color: white; } .difficulty-hard { background: #F44336; color: white; } .memory-grid-container { padding: 25px; background: #f5f7fa; } .memory-grid { display: grid; gap: 12px; margin: 0 auto; max-width: 800px; } .memory-card { aspect-ratio: 1; background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%); border-radius: 10px; display: flex; align-items: center; justify-content: center; cursor: pointer; transform-style: preserve-3d; transition: transform 0.6s; font-size: 2.5em; position: relative; user-select: none; } .memory-card .card-front, .memory-card .card-back { position: absolute; width: 100%; height: 100%; backface-visibility: hidden; border-radius: 10px; display: flex; align-items: center; justify-content: center; } .memory-card .card-front { background: white; color: #333; transform: rotateY(180deg); } .memory-card .card-back { background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%); color: white; } .memory-card.flipped { transform: rotateY(180deg); } .memory-card.matched { transform: rotateY(180deg); cursor: default; } .memory-card.matched .card-front { background: #e8f5e9; border: 3px solid #4CAF50; } .memory-card.hint { animation: hint-pulse 1s ease-in-out; } @keyframes hint-pulse { 0%, 100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.7); } 50% { box-shadow: 0 0 0 10px rgba(76, 175, 80, 0); } } .memory-controls { padding: 20px; background: white; display: flex; justify-content: center; gap: 15px; border-top: 1px solid #eee; } .memory-btn { padding: 12px 25px; border: none; border-radius: 8px; cursor: pointer; font-size: 1em; font-weight: 600; transition: all 0.3s ease; } .start-btn { background: #4CAF50; color: white; } .restart-btn { background: #2196F3; color: white; } .hint-btn { background: #FF9800; color: white; } .memory-btn:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0,0,0,0.1); } .memory-btn:disabled { opacity: 0.5; cursor: not-allowed; } .memory-results { padding: 30px; text-align: center; background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); } .stats-summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 20px; margin: 30px 0; max-width: 600px; margin-left: auto; margin-right: auto; } .stat-item { background: white; padding: 20px; border-radius: 10px; box-shadow: 0 3px 10px rgba(0,0,0,0.08); } .stat-label { display: block; color: #666; font-size: 0.9em; margin-bottom: 5px; } .stat-value { font-size: 1.8em; font-weight: 700; color: #2575fc; } .performance-rating { font-size: 1.3em; margin: 20px 0; padding: 15px; background: white; border-radius: 10px; display: inline-block; min-width: 300px; } .results-controls { margin-top: 30px; display: flex; justify-content: center; gap: 15px; } .play-again-btn { background: #4CAF50; color: white; } .share-results-btn { background: #2196F3; color: white; } 3.3 添加记忆游戏JavaScript逻辑 在lightgames.js文件中添加记忆游戏的交互逻辑: // 记忆匹配游戏逻辑 function initMemoryGame(gameContainer) { const cardsData = JSON.parse(gameContainer.querySelector('.memory-cards-data').textContent); const pairs = parseInt(gameContainer.getAttribute('data-pairs')); const timeLimit = parseInt(gameContainer.getAttribute('data-time')); let cards = []; let flippedCards = []; let matchedPairs = 0; let moves = 0; let timeLeft = timeLimit; let gameStarted = false; let gameTimer; let canFlip = true; // DOM元素 const gridEl = gameContainer.querySelector('.memory-grid'); const movesEl = gameContainer.querySelector('.moves strong'); const timerEl = gameContainer.querySelector('.timer strong'); const matchesEl = gameContainer.querySelector('.matches strong'); const startBtn = gameContainer.querySelector('.start-btn'); const restartBtn = gameContainer.querySelector('.restart-btn'); const hintBtn = gameContainer.querySelector('.hint-btn'); const resultsEl = gameContainer.querySelector('.memory-results'); const timeUsedEl = gameContainer.querySelector('.time-used'); const totalMovesEl = gameContainer.querySelector('.total-moves'); const accuracyEl = gameContainer.querySelector('.accuracy'); const finalScoreEl = gameContainer.querySelector('.final-score'); const performanceEl = gameContainer.querySelector('.performance-rating'); const playAgainBtn = gameContainer.querySelector('.play-again-btn'); const shareResultsBtn = gameContainer.querySelector('.share-results-btn'); // 初始化游戏板 function initGameBoard() { gridEl.innerHTML = ''; cards = [...cardsData]; cards.forEach((cardData, index) => { const card = document.createElement('div'); card.className = 'memory-card'; card.dataset.id = cardData.id; card.dataset.value = cardData.value; const cardFront = document.createElement('div'); cardFront.className = 'card-front'; cardFront.textContent = cardData.value; const cardBack = document.createElement('div'); cardBack.className = 'card-back'; cardBack.textContent = '?'; card.appendChild(cardFront); card.appendChild(cardBack); card.addEventListener('click', () => flipCard(card)); gridEl.appendChild(card); }); updateStats(); } // 开始游戏 function startGame() { if (gameStarted) return; gameStarted = true; startBtn.disabled = true; restartBtn.disabled = false; hintBtn.disabled = false; // 开始计时 startTimer(); // 短暂显示所有卡片 showAllCardsBriefly(); } // 显示所有卡片(游戏开始时) function showAllCardsBriefly() { const allCards = gridEl.querySelectorAll('.memory-card'); allCards.forEach(card => { card.classList.add('flipped'); }); setTimeout(() => { allCards.forEach(card => { card.classList.remove('flipped'); }); canFlip = true; }, 2000); } // 开始计时器 function startTimer() { clearInterval(gameTimer); timeLeft = timeLimit; updateTimerDisplay(); gameTimer = setInterval(() => { timeLeft--; updateTimerDisplay(); if (timeLeft <= 0) { endGame(false); } }, 1000); } // 更新计时器显示 function updateTimerDisplay() { timerEl.textContent = timeLeft; if (timeLeft <= 10) { timerEl.style.color = '#f44336'; timerEl.style.animation = timeLeft <= 5 ? 'pulse 0.5s infinite' : 'none'; } else { timerEl.style.color = ''; timerEl.style.animation = ''; } } // 翻转卡片 function flipCard(card) { if (!canFlip || !gameStarted) return; if (card.classList.contains('flipped') || card.classList.contains('matched')) return; if (flippedCards.length >= 2) return; card.classList.add('flipped'); flippedCards.push(card); if (flippedCards.length === 2) { moves++; movesEl.textContent = moves; canFlip = false; // 检查是否匹配 const card1 = flippedCards[0]; const card2 = flippedCards[1]; if (card1.dataset.value === card2.dataset.value) { // 匹配成功 setTimeout(() => { card1.classList.add('matched'); card2.classList.add('matched'); flippedCards = []; matchedPairs++; matchesEl.textContent = matchedPairs; canFlip = true; // 检查游戏是否完成 if (matchedPairs === pairs) { endGame(true); } }, 500); } else { // 不匹配,翻回去 setTimeout(() => { card1.classList.remove('flipped'); card2.classList.remove('flipped'); flippedCards = []; canFlip = true; }, 1000); } } } // 提示功能 function provideHint() { if (!gameStarted || flippedCards.length > 0) return; // 找到两个未匹配的相同卡片
发表评论手把手教程:在WordPress中集成网站Cookie合规性管理与用户隐私同意控件 引言:数字时代的隐私合规挑战 在当今数字化时代,随着全球数据保护法规的不断完善,网站隐私合规性已成为每个网站所有者必须面对的重要课题。从欧盟的《通用数据保护条例》(GDPR)到加州的《消费者隐私法案》(CCPA),再到中国的《个人信息保护法》,全球各地都在加强对用户隐私的保护。对于使用WordPress构建的网站而言,如何有效管理Cookie和获取用户隐私同意,不仅是一项法律义务,更是建立用户信任的关键。 本教程将深入探讨如何通过WordPress代码二次开发,实现专业的Cookie合规性管理与用户隐私同意控件,同时集成常用互联网小工具功能。我们将从基础概念入手,逐步深入到具体实现,为您提供一套完整、实用的解决方案。 第一部分:理解Cookie合规性的基本要求 1.1 什么是Cookie合规性? Cookie合规性指的是网站在使用Cookie和其他跟踪技术时,必须遵守相关隐私法规的要求。这主要包括: 知情同意原则:在设置非必要Cookie前,必须获得用户的明确同意 透明度原则:清晰告知用户网站使用了哪些Cookie及其目的 控制权原则:允许用户随时撤回同意或调整Cookie偏好 数据最小化原则:只收集实现特定目的所需的最少数据 1.2 主要隐私法规概览 GDPR(欧盟通用数据保护条例):适用于处理欧盟公民数据的任何组织,无论其所在地 CCPA(加州消费者隐私法案):保护加州居民的个人信息权利 PIPL(中国个人信息保护法):规范在中国境内处理个人信息的活动 ePrivacy指令:专门规范电子通信隐私,包括Cookie使用 1.3 WordPress网站的特殊考虑 WordPress作为内容管理系统,本身及其插件、主题都可能设置各种Cookie。常见的包括: 会话Cookie(用于用户登录状态) 评论功能Cookie 统计分析Cookie(如Google Analytics) 社交媒体集成Cookie 广告跟踪Cookie 第二部分:规划Cookie合规解决方案 2.1 功能需求分析 一个完整的Cookie合规解决方案应包含以下核心功能: Cookie横幅/弹出窗口:首次访问时显示,请求用户同意 Cookie偏好中心:允许用户详细管理各类Cookie 同意记录与证明:存储用户同意状态,以便审计 脚本加载控制:根据用户同意状态有条件加载第三方脚本 自动阻止功能:在获得同意前阻止非必要Cookie 定期重新同意:根据法规要求定期更新用户同意 2.2 技术架构设计 我们将采用模块化设计,创建以下核心组件: 主控制器类:协调所有功能模块 前端展示模块:处理横幅和偏好中心的UI 同意管理模块:存储和管理用户同意状态 脚本控制模块:根据同意状态控制第三方脚本加载 设置管理模块:提供后台配置界面 小工具集成模块:扩展常用互联网工具功能 2.3 数据库设计 我们需要创建数据库表来存储: 用户同意记录 Cookie分类和描述 第三方服务配置 同意历史记录 第三部分:环境准备与基础设置 3.1 开发环境搭建 在开始编码前,请确保您已准备好: 本地WordPress开发环境:可以使用Local by Flywheel、XAMPP或Docker 代码编辑器:推荐VS Code、PHPStorm或Sublime Text 浏览器开发者工具:用于调试前端代码 Git版本控制系统:管理代码变更 3.2 创建WordPress插件 我们将创建一个独立的WordPress插件来实现所有功能: <?php /** * Plugin Name: Advanced Cookie Consent Manager * Plugin URI: https://yourwebsite.com/ * Description: 完整的Cookie合规性与隐私同意管理解决方案 * Version: 1.0.0 * Author: Your Name * License: GPL v2 or later * Text Domain: advanced-cookie-consent */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('ACCM_VERSION', '1.0.0'); define('ACCM_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('ACCM_PLUGIN_URL', plugin_dir_url(__FILE__)); define('ACCM_PLUGIN_BASENAME', plugin_basename(__FILE__)); 3.3 插件目录结构 创建以下目录结构: advanced-cookie-consent/ ├── includes/ │ ├── class-main-controller.php │ ├── class-frontend-ui.php │ ├── class-consent-manager.php │ ├── class-script-controller.php │ ├── class-settings-manager.php │ └── class-widget-integration.php ├── assets/ │ ├── css/ │ │ ├── frontend.css │ │ └── admin.css │ ├── js/ │ │ ├── frontend.js │ │ └── admin.js │ └── images/ ├── languages/ ├── templates/ │ ├── cookie-banner.php │ └── preference-center.php └── advanced-cookie-consent.php 第四部分:核心功能实现 4.1 主控制器类实现 <?php // includes/class-main-controller.php class ACCM_Main_Controller { private static $instance = null; private $frontend_ui; private $consent_manager; private $script_controller; private $settings_manager; private $widget_integration; public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } private function __construct() { $this->load_dependencies(); $this->init_hooks(); } private function load_dependencies() { require_once ACCM_PLUGIN_DIR . 'includes/class-frontend-ui.php'; require_once ACCM_PLUGIN_DIR . 'includes/class-consent-manager.php'; require_once ACCM_PLUGIN_DIR . 'includes/class-script-controller.php'; require_once ACCM_PLUGIN_DIR . 'includes/class-settings-manager.php'; require_once ACCM_PLUGIN_DIR . 'includes/class-widget-integration.php'; $this->frontend_ui = new ACCM_Frontend_UI(); $this->consent_manager = new ACCM_Consent_Manager(); $this->script_controller = new ACCM_Script_Controller(); $this->settings_manager = new ACCM_Settings_Manager(); $this->widget_integration = new ACCM_Widget_Integration(); } private function init_hooks() { // 激活/停用插件钩子 register_activation_hook(__FILE__, array($this, 'activate_plugin')); register_deactivation_hook(__FILE__, array($this, 'deactivate_plugin')); // 初始化钩子 add_action('init', array($this, 'init')); // 管理界面钩子 add_action('admin_menu', array($this->settings_manager, 'add_admin_menu')); add_action('admin_init', array($this->settings_manager, 'register_settings')); // 前端钩子 add_action('wp_enqueue_scripts', array($this->frontend_ui, 'enqueue_assets')); add_action('wp_footer', array($this->frontend_ui, 'render_cookie_banner')); // 脚本控制钩子 add_action('wp_head', array($this->script_controller, 'add_script_control_scripts'), 1); } public function init() { // 加载文本域 load_plugin_textdomain('advanced-cookie-consent', false, dirname(ACCM_PLUGIN_BASENAME) . '/languages'); // 初始化组件 $this->consent_manager->init(); $this->widget_integration->init(); } public function activate_plugin() { // 创建数据库表 $this->consent_manager->create_tables(); // 设置默认选项 $this->settings_manager->set_default_options(); // 刷新重写规则 flush_rewrite_rules(); } public function deactivate_plugin() { // 清理临时数据 flush_rewrite_rules(); } } 4.2 前端UI与Cookie横幅实现 <?php // includes/class-frontend-ui.php class ACCM_Frontend_UI { private $consent_manager; public function __construct() { $this->consent_manager = ACCM_Consent_Manager::get_instance(); } public function enqueue_assets() { // 前端样式 wp_enqueue_style( 'accm-frontend-style', ACCM_PLUGIN_URL . 'assets/css/frontend.css', array(), ACCM_VERSION ); // 前端脚本 wp_enqueue_script( 'accm-frontend-script', ACCM_PLUGIN_URL . 'assets/js/frontend.js', array('jquery'), ACCM_VERSION, true ); // 本地化脚本 wp_localize_script('accm-frontend-script', 'accm_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('accm_nonce'), 'consent_categories' => $this->consent_manager->get_cookie_categories(), 'strings' => array( 'saving' => __('保存中...', 'advanced-cookie-consent'), 'saved' => __('设置已保存', 'advanced-cookie-consent'), 'error' => __('保存时出错,请重试', 'advanced-cookie-consent') ) )); } public function render_cookie_banner() { // 检查是否已获得同意 if ($this->consent_manager->has_consent()) { return; } // 获取设置 $settings = get_option('accm_settings', array()); // 包含横幅模板 include ACCM_PLUGIN_DIR . 'templates/cookie-banner.php'; } public function render_preference_center() { // 获取Cookie分类 $categories = $this->consent_manager->get_cookie_categories(); // 获取用户当前偏好 $user_preferences = $this->consent_manager->get_user_preferences(); // 包含偏好中心模板 include ACCM_PLUGIN_DIR . 'templates/preference-center.php'; } } 4.3 Cookie横幅模板 <?php // templates/cookie-banner.php $settings = get_option('accm_settings', array()); $banner_title = isset($settings['banner_title']) ? $settings['banner_title'] : __('Cookie设置', 'advanced-cookie-consent'); $banner_text = isset($settings['banner_text']) ? $settings['banner_text'] : __('我们使用Cookie来提升您的浏览体验,提供个性化内容并分析流量。点击"接受"即表示您同意我们使用所有Cookie。', 'advanced-cookie-consent'); $accept_text = isset($settings['accept_text']) ? $settings['accept_text'] : __('接受所有', 'advanced-cookie-consent'); $reject_text = isset($settings['reject_text']) ? $settings['reject_text'] : __('拒绝非必要', 'advanced-cookie-consent'); $preferences_text = isset($settings['preferences_text']) ? $settings['preferences_text'] : __('自定义设置', 'advanced-cookie-consent'); $privacy_policy_url = isset($settings['privacy_policy_url']) ? $settings['privacy_policy_url'] : get_privacy_policy_url(); $privacy_policy_text = isset($settings['privacy_policy_text']) ? $settings['privacy_policy_text'] : __('隐私政策', 'advanced-cookie-consent'); ?> <div id="accm-cookie-banner" class="accm-cookie-banner" style="display: none;"> <div class="accm-banner-content"> <div class="accm-banner-text"> <h3><?php echo esc_html($banner_title); ?></h3> <p><?php echo esc_html($banner_text); ?></p> <?php if ($privacy_policy_url): ?> <p class="accm-privacy-link"> <a href="<?php echo esc_url($privacy_policy_url); ?>" target="_blank"> <?php echo esc_html($privacy_policy_text); ?> </a> </p> <?php endif; ?> </div> <div class="accm-banner-buttons"> <button type="button" class="accm-btn accm-btn-preferences"> <?php echo esc_html($preferences_text); ?> </button> <button type="button" class="accm-btn accm-btn-reject"> <?php echo esc_html($reject_text); ?> </button> <button type="button" class="accm-btn accm-btn-accept accm-btn-primary"> <?php echo esc_html($accept_text); ?> </button> </div> </div> </div> <div id="accm-preference-modal" class="accm-modal" style="display: none;"> <div class="accm-modal-content"> <div class="accm-modal-header"> <h3><?php _e('Cookie偏好设置', 'advanced-cookie-consent'); ?></h3> <button type="button" class="accm-modal-close">×</button> </div> <div class="accm-modal-body"> <?php $this->render_preference_center(); ?> </div> <div class="accm-modal-footer"> <button type="button" class="accm-btn accm-btn-save-preferences accm-btn-primary"> <?php _e('保存设置', 'advanced-cookie-consent'); ?> </button> </div> </div> </div> 4.4 同意管理模块 <?php // includes/class-consent-manager.php class ACCM_Consent_Manager { private static $instance = null; private $cookie_name = 'accm_consent'; private $cookie_expiry = 365; // 天数 public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } public function init() { // 处理AJAX请求 add_action('wp_ajax_accm_save_consent', array($this, 'ajax_save_consent')); add_action('wp_ajax_nopriv_accm_save_consent', array($this, 'ajax_save_consent')); // 检查并处理Cookie同意 $this->check_consent_cookie(); } public function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'accm_consent_logs'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id bigint(20) NOT NULL AUTO_INCREMENT, consent_id varchar(32) NOT NULL, user_id bigint(20) DEFAULT 0, user_ip varchar(45) DEFAULT '', user_agent text, consent_data text NOT NULL, consent_date datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY consent_id (consent_id), KEY user_id (user_id), KEY consent_date (consent_date) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } public function get_cookie_categories() { $default_categories = array( 'necessary' => array( 'name' => __('必要', 'advanced-cookie-consent'), 'description' => __('这些Cookie对于网站的基本功能是必需的,无法关闭。', 'advanced-cookie-consent'), 'required' => true, 'default' => true ), 'preferences' => array( 'name' => __('偏好设置', 'advanced-cookie-consent'), 'description' => __('这些Cookie允许网站记住您的选择和偏好。', 'advanced-cookie-consent'), 'required' => false, 'default' => true ), 'analytics' => array( 'name' => __('统计分析', 'advanced-cookie-consent'), 'description' => __('这些Cookie帮助我们了解访问者如何与网站互动。', 'advanced-cookie-consent'), 'required' => false, 'default' => true ), 'marketing' => array( 'name' => __('营销', 'advanced-cookie-consent'), 'description' => __('这些Cookie用于跟踪广告效果和个性化广告。', 'advanced-cookie-consent'), 'required' => false, 'default' => false ) ); // 允许通过过滤器添加或修改分类 return apply_filters('accm_cookie_categories', $default_categories); } public function has_consent($category = '') { $consent_data = $this->get_consent_data(); if (empty($consent_data)) { return false; } if (empty($category)) { return !empty($consent_data); } $categories = $this->get_cookie_categories(); if (!isset($categories[$category])) { return false; } // 必要Cookie始终视为已同意 手把手教程:在WordPress中集成网站Cookie合规性管理与用户隐私同意控件(续) 第四部分:核心功能实现(续) 4.4 同意管理模块(续) <?php // includes/class-consent-manager.php(续) if ($categories[$category]['required']) { return true; } return isset($consent_data[$category]) && $consent_data[$category] === true; } private function get_consent_data() { static $consent_data = null; if ($consent_data !== null) { return $consent_data; } // 首先检查Cookie if (isset($_COOKIE[$this->cookie_name])) { $cookie_data = json_decode(stripslashes($_COOKIE[$this->cookie_name]), true); if (is_array($cookie_data)) { $consent_data = $cookie_data; return $consent_data; } } // 如果没有Cookie,检查默认设置 $settings = get_option('accm_settings', array()); $default_consent = isset($settings['default_consent']) ? $settings['default_consent'] : 'none'; if ($default_consent === 'all') { $categories = $this->get_cookie_categories(); $consent_data = array(); foreach ($categories as $key => $category) { $consent_data[$key] = true; } } else { $consent_data = array(); } return $consent_data; } public function get_user_preferences() { $consent_data = $this->get_consent_data(); $categories = $this->get_cookie_categories(); $preferences = array(); foreach ($categories as $key => $category) { $preferences[$key] = array( 'name' => $category['name'], 'description' => $category['description'], 'required' => $category['required'], 'enabled' => $this->has_consent($key) ); } return $preferences; } public function ajax_save_consent() { // 验证nonce if (!check_ajax_referer('accm_nonce', 'nonce', false)) { wp_die(json_encode(array( 'success' => false, 'message' => __('安全验证失败', 'advanced-cookie-consent') ))); } // 获取并验证数据 $consent_data = isset($_POST['consent_data']) ? $_POST['consent_data'] : array(); if (!is_array($consent_data)) { wp_die(json_encode(array( 'success' => false, 'message' => __('无效的数据格式', 'advanced-cookie-consent') ))); } // 验证分类 $categories = $this->get_cookie_categories(); $validated_data = array(); foreach ($categories as $key => $category) { if ($category['required']) { $validated_data[$key] = true; } else { $validated_data[$key] = isset($consent_data[$key]) && $consent_data[$key] === 'true'; } } // 生成唯一ID $consent_id = wp_generate_uuid4(); // 记录同意 $this->log_consent($consent_id, $validated_data); // 设置Cookie $this->set_consent_cookie($consent_id, $validated_data); // 返回成功响应 wp_die(json_encode(array( 'success' => true, 'message' => __('设置已保存', 'advanced-cookie-consent'), 'consent_id' => $consent_id ))); } private function log_consent($consent_id, $consent_data) { global $wpdb; $table_name = $wpdb->prefix . 'accm_consent_logs'; $user_id = get_current_user_id(); $user_ip = $this->get_user_ip(); $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; $wpdb->insert( $table_name, array( 'consent_id' => $consent_id, 'user_id' => $user_id, 'user_ip' => $user_ip, 'user_agent' => $user_agent, 'consent_data' => json_encode($consent_data) ), array('%s', '%d', '%s', '%s', '%s') ); } private function set_consent_cookie($consent_id, $consent_data) { $cookie_data = array( 'consent_id' => $consent_id, 'categories' => $consent_data, 'timestamp' => time() ); $cookie_value = json_encode($cookie_data); // 设置Cookie setcookie( $this->cookie_name, $cookie_value, time() + ($this->cookie_expiry * DAY_IN_SECONDS), COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true // HttpOnly ); // 立即在本次请求中可用 $_COOKIE[$this->cookie_name] = $cookie_value; } private function get_user_ip() { $ip = ''; if (!empty($_SERVER['HTTP_CLIENT_IP'])) { $ip = $_SERVER['HTTP_CLIENT_IP']; } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { $ip = $_SERVER['HTTP_X_FORWARDED_FOR']; } else { $ip = $_SERVER['REMOTE_ADDR']; } return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : ''; } private function check_consent_cookie() { // 检查Cookie是否需要更新 if (isset($_COOKIE[$this->cookie_name])) { $cookie_data = json_decode(stripslashes($_COOKIE[$this->cookie_name]), true); if (is_array($cookie_data) && isset($cookie_data['timestamp'])) { $cookie_age = time() - $cookie_data['timestamp']; $renewal_period = 365 * DAY_IN_SECONDS; // 一年后重新请求同意 if ($cookie_age > $renewal_period) { // 删除旧Cookie,强制重新同意 setcookie( $this->cookie_name, '', time() - 3600, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); unset($_COOKIE[$this->cookie_name]); } } } } } 4.5 脚本控制模块 <?php // includes/class-script-controller.php class ACCM_Script_Controller { private $consent_manager; private $blocked_scripts = array(); public function __construct() { $this->consent_manager = ACCM_Consent_Manager::get_instance(); // 注册脚本控制钩子 add_action('wp_head', array($this, 'add_script_control_scripts'), 1); add_filter('script_loader_tag', array($this, 'filter_script_tags'), 10, 3); add_action('wp_footer', array($this, 'load_blocked_scripts'), 99); } public function add_script_control_scripts() { // 添加脚本控制逻辑 ?> <script type="text/javascript"> window.accmScriptControl = { categories: <?php echo json_encode($this->consent_manager->get_cookie_categories()); ?>, hasConsent: function(category) { var consentData = this.getConsentData(); if (!consentData) return false; // 必要脚本始终允许 if (this.categories[category] && this.categories[category].required) { return true; } return consentData.categories && consentData.categories[category] === true; }, getConsentData: function() { try { var cookieValue = document.cookie .split('; ') .find(row => row.startsWith('accm_consent=')); if (cookieValue) { return JSON.parse(decodeURIComponent(cookieValue.split('=')[1])); } } catch(e) { console.error('Error parsing consent cookie:', e); } return null; }, loadScript: function(src, category, attributes) { if (!this.hasConsent(category)) { console.log('Script blocked due to missing consent:', src, 'Category:', category); return false; } var script = document.createElement('script'); script.src = src; if (attributes) { for (var attr in attributes) { if (attributes.hasOwnProperty(attr)) { script.setAttribute(attr, attributes[attr]); } } } document.head.appendChild(script); return true; } }; // 重写window.dataLayer.push以延迟Google Tag Manager if (typeof window.dataLayer === 'undefined') { window.dataLayer = []; } var originalDataLayerPush = window.dataLayer.push; window.dataLayer.push = function() { if (!accmScriptControl.hasConsent('analytics') && !accmScriptControl.hasConsent('marketing')) { console.log('GTM event blocked due to missing consent'); return; } return originalDataLayerPush.apply(this, arguments); }; </script> <?php } public function filter_script_tags($tag, $handle, $src) { // 获取脚本的Cookie类别 $script_categories = $this->get_script_categories($handle); if (empty($script_categories)) { return $tag; } // 检查是否所有需要的类别都有同意 $block_script = false; foreach ($script_categories as $category) { if (!$this->consent_manager->has_consent($category)) { $block_script = true; break; } } if ($block_script) { // 存储被阻止的脚本以便稍后加载 $this->blocked_scripts[] = array( 'tag' => $tag, 'handle' => $handle, 'src' => $src, 'categories' => $script_categories ); // 返回占位符或空字符串 return "<!-- Script '$handle' blocked due to missing cookie consent -->n"; } return $tag; } private function get_script_categories($handle) { $script_categories = array( 'google-analytics' => array('analytics'), 'gtm4wp' => array('analytics', 'marketing'), 'facebook-pixel' => array('marketing'), 'twitter-widgets' => array('marketing', 'preferences'), 'youtube-embed' => array('preferences'), 'vimeo-embed' => array('preferences'), 'google-maps' => array('preferences') ); // 允许通过过滤器添加或修改 $script_categories = apply_filters('accm_script_categories', $script_categories); return isset($script_categories[$handle]) ? $script_categories[$handle] : array(); } public function load_blocked_scripts() { if (empty($this->blocked_scripts)) { return; } ?> <script type="text/javascript"> document.addEventListener('DOMContentLoaded', function() { var blockedScripts = <?php echo json_encode($this->blocked_scripts); ?>; blockedScripts.forEach(function(scriptInfo) { var shouldLoad = true; scriptInfo.categories.forEach(function(category) { if (!accmScriptControl.hasConsent(category)) { shouldLoad = false; } }); if (shouldLoad) { // 创建并插入脚本 var script = document.createElement('script'); script.src = scriptInfo.src; // 复制原始属性 var regex = /(w+)=["']([^"']*)["']/g; var match; while ((match = regex.exec(scriptInfo.tag)) !== null) { if (match[1] !== 'src') { script.setAttribute(match[1], match[2]); } } document.head.appendChild(script); console.log('Previously blocked script loaded:', scriptInfo.handle); } }); }); </script> <?php } public function add_third_party_service($service_name, $script_code, $categories) { add_action('wp_footer', function() use ($service_name, $script_code, $categories) { $can_load = true; foreach ($categories as $category) { if (!$this->consent_manager->has_consent($category)) { $can_load = false; break; } } if ($can_load) { echo $script_code; } else { // 存储以便稍后加载 $this->blocked_scripts[] = array( 'tag' => $script_code, 'handle' => $service_name, 'categories' => $categories ); } }, 10); } } 4.6 设置管理模块 <?php // includes/class-settings-manager.php class ACCM_Settings_Manager { private $settings_page; private $settings_group = 'accm_settings_group'; private $settings_section = 'accm_settings_section'; private $option_name = 'accm_settings'; public function add_admin_menu() { $this->settings_page = add_options_page( __('Cookie同意设置', 'advanced-cookie-consent'), __('Cookie同意', 'advanced-cookie-consent'), 'manage_options', 'advanced-cookie-consent', array($this, 'render_settings_page') ); // 添加设置页面样式和脚本 add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets')); } public function enqueue_admin_assets($hook) { if ($hook !== $this->settings_page) { return; } wp_enqueue_style( 'accm-admin-style', ACCM_PLUGIN_URL . 'assets/css/admin.css', array(), ACCM_VERSION ); wp_enqueue_script( 'accm-admin-script', ACCM_PLUGIN_URL . 'assets/js/admin.js', array('jquery', 'wp-color-picker'), ACCM_VERSION, true ); // 启用颜色选择器 wp_enqueue_style('wp-color-picker'); } public function register_settings() { register_setting( $this->settings_group, $this->option_name, array($this, 'sanitize_settings') ); add_settings_section( $this->settings_section, __('基本设置', 'advanced-cookie-consent'), array($this, 'render_section_header'), 'advanced-cookie-consent' ); // 添加设置字段 $this->add_settings_fields(); } private function add_settings_fields() { $fields = array( array( 'id' => 'banner_title', 'title' => __('横幅标题', 'advanced-cookie-consent'), 'callback' => 'render_text_field', 'args' => array( 'description' => __('Cookie横幅的标题', 'advanced-cookie-consent'), 'default' => __('Cookie设置', 'advanced-cookie-consent') ) ), array( 'id' => 'banner_text', 'title' => __('横幅文本', 'advanced-cookie-consent'), 'callback' => 'render_textarea_field', 'args' => array( 'description' => __('Cookie横幅的主要说明文本', 'advanced-cookie-consent'), 'default' => __('我们使用Cookie来提升您的浏览体验,提供个性化内容并分析流量。点击"接受"即表示您同意我们使用所有Cookie。', 'advanced-cookie-consent'), 'rows' => 4 ) ), array( 'id' => 'banner_position', 'title' => __('横幅位置', 'advanced-cookie-consent'), 'callback' => 'render_select_field', 'args' => array( 'description' => __('选择Cookie横幅的显示位置', 'advanced-cookie-consent'), 'options' => array( 'bottom' => __('底部', 'advanced-cookie-consent'), 'top' => __('顶部', 'advanced-cookie-consent'), 'bottom-left' => __('左下角', 'advanced-cookie-consent'), 'bottom-right' => __('右下角', 'advanced-cookie-consent') ), 'default' => 'bottom' ) ), array( 'id' => 'banner_style', 'title' => __('横幅样式', 'advanced-cookie-consent'), 'callback' => 'render_select_field', 'args' => array( 'description' => __('选择Cookie横幅的视觉样式', 'advanced-cookie-consent'), 'options' => array( 'light' => __('浅色', 'advanced-cookie-consent'), 'dark' => __('深色', 'advanced-cookie-consent'), 'minimal' => __('极简', 'advanced-cookie-consent') ), 'default' => 'light' ) ), array( 'id' => 'primary_color', 'title' => __('主色调', 'advanced-cookie-consent'), 'callback' => 'render_color_field', 'args' => array(
发表评论详细教程:为WordPress网站打造内嵌在线简易视频编辑与短片制作工具 引言:为什么网站需要内置视频编辑功能? 在当今数字内容爆炸的时代,视频已成为最受欢迎的内容形式之一。据统计,超过85%的互联网用户每周都会观看在线视频内容。对于内容创作者、营销人员和网站所有者来说,能够快速制作和编辑视频已成为一项核心竞争力。 然而,传统的视频编辑流程往往复杂且耗时:用户需要下载专业软件、学习复杂操作、导出文件后再上传到网站。这一过程不仅效率低下,还可能导致用户流失。通过在WordPress网站中内置简易视频编辑工具,我们可以: 大幅降低用户制作视频的门槛 提高用户参与度和内容产出率 创造独特的用户体验和竞争优势 减少对外部服务的依赖,保护用户数据隐私 本教程将详细指导您如何通过WordPress代码二次开发,为网站添加一个功能完整的在线简易视频编辑与短片制作工具。 第一部分:项目规划与技术选型 1.1 功能需求分析 在开始开发前,我们需要明确工具应具备的核心功能: 基础编辑功能: 视频裁剪与分割 多视频片段拼接 添加背景音乐和音效 文本叠加与字幕添加 基本滤镜和色彩调整 高级功能(可选): 绿幕抠像(色度键控) 转场效果 动画元素添加 语音转字幕 模板化快速制作 输出选项: 多种分辨率支持(480p、720p、1080p) 多种格式输出(MP4、WebM、GIF) 直接发布到网站媒体库 1.2 技术架构设计 我们将采用前后端分离的架构: 前端技术栈: HTML5 Video API:处理视频播放和基础操作 Canvas API:实现视频帧处理和滤镜效果 Web Audio API:处理音频混合 FFmpeg.wasm:在浏览器中实现视频转码和合成 React/Vue.js(可选):构建交互式UI 后端技术栈: WordPress REST API:处理用户认证和数据存储 PHP GD库/ImageMagick:服务器端图像处理 自定义数据库表:存储用户项目和编辑历史 关键技术挑战与解决方案: 浏览器性能限制:采用分段处理和Web Worker 大文件处理:使用流式处理和分块上传 跨浏览器兼容性:功能检测和渐进增强策略 第二部分:开发环境搭建与基础配置 2.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('VIDEO_EDITOR_VERSION', '1.0.0'); define('VIDEO_EDITOR_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('VIDEO_EDITOR_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 class Video_Editor_Plugin { private static $instance = null; public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } private function __construct() { $this->init_hooks(); } private function init_hooks() { // 注册激活和停用钩子 register_activation_hook(__FILE__, array($this, 'activate')); register_deactivation_hook(__FILE__, array($this, 'deactivate')); // 初始化 add_action('init', array($this, 'init')); // 管理菜单 add_action('admin_menu', array($this, 'add_admin_menu')); // 前端资源 add_action('wp_enqueue_scripts', array($this, 'enqueue_frontend_assets')); // 短代码 add_shortcode('video_editor', array($this, 'video_editor_shortcode')); } public function activate() { // 创建必要的数据库表 $this->create_database_tables(); // 设置默认选项 update_option('video_editor_max_upload_size', 500); // MB update_option('video_editor_allowed_formats', 'mp4,webm,mov,avi'); update_option('video_editor_default_quality', '720p'); } public function deactivate() { // 清理临时文件 $this->cleanup_temp_files(); } public function init() { // 注册自定义文章类型(如果需要) // 初始化REST API端点 add_action('rest_api_init', array($this, 'register_rest_routes')); } // 其他方法将在后续部分实现 } // 启动插件 Video_Editor_Plugin::get_instance(); ?> 2.2 创建必要的数据库表 我们需要创建表来存储用户的项目数据: private function create_database_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'video_editor_projects'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, project_name varchar(255) NOT NULL, project_data longtext NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, status varchar(20) DEFAULT 'draft', PRIMARY KEY (id), KEY user_id (user_id), KEY status (status) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 创建临时文件记录表 $temp_table = $wpdb->prefix . 'video_editor_temp_files'; $sql_temp = "CREATE TABLE IF NOT EXISTS $temp_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, file_hash varchar(64) NOT NULL, file_path varchar(500) NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, expires_at datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY file_hash (file_hash), KEY expires_at (expires_at) ) $charset_collate;"; dbDelta($sql_temp); } 第三部分:前端编辑器界面开发 3.1 构建编辑器HTML结构 创建编辑器的主要界面结构: <!-- 在插件目录中创建templates/editor-frontend.php --> <div id="video-editor-app" class="video-editor-container"> <!-- 顶部工具栏 --> <div class="editor-toolbar"> <div class="toolbar-left"> <button id="btn-new-project" class="editor-btn"> <i class="icon-new"></i> 新建项目 </button> <button id="btn-save-project" class="editor-btn"> <i class="icon-save"></i> 保存项目 </button> <button id="btn-export" class="editor-btn btn-primary"> <i class="icon-export"></i> 导出视频 </button> </div> <div class="toolbar-right"> <div class="project-name"> <input type="text" id="project-name" placeholder="项目名称" value="未命名项目"> </div> </div> </div> <!-- 主工作区 --> <div class="editor-workspace"> <!-- 左侧资源面板 --> <div class="panel-left"> <div class="panel-tabs"> <button class="panel-tab active" data-tab="media">媒体库</button> <button class="panel-tab" data-tab="text">文字</button> <button class="panel-tab" data-tab="audio">音频</button> <button class="panel-tab" data-tab="effects">特效</button> </div> <div class="panel-content"> <!-- 媒体库内容 --> <div id="tab-media" class="tab-content active"> <div class="media-actions"> <button id="btn-upload-media" class="action-btn"> <i class="icon-upload"></i> 上传媒体 </button> <button id="btn-record-video" class="action-btn"> <i class="icon-record"></i> 录制视频 </button> </div> <div class="media-library"> <!-- 动态加载媒体项 --> </div> </div> <!-- 其他标签页内容 --> <!-- ... --> </div> </div> <!-- 中央预览区 --> <div class="panel-center"> <div class="video-preview-container"> <div class="preview-controls"> <button id="btn-play" class="control-btn"> <i class="icon-play"></i> </button> <div class="timeline-container"> <div class="timeline-scrubber"></div> <div class="timeline-track" id="video-timeline"> <!-- 时间轴轨道 --> </div> </div> <div class="time-display"> <span id="current-time">00:00</span> / <span id="duration">00:00</span> </div> </div> <div class="video-canvas-container"> <canvas id="video-canvas" width="1280" height="720"></canvas> <video id="source-video" style="display:none;" crossorigin="anonymous"></video> </div> </div> </div> <!-- 右侧属性面板 --> <div class="panel-right"> <div class="property-panel"> <h3>视频属性</h3> <div class="property-group"> <label>裁剪</label> <div class="crop-controls"> <input type="number" id="crop-start" placeholder="开始时间(秒)" min="0"> <input type="number" id="crop-end" placeholder="结束时间(秒)" min="0"> <button id="btn-apply-crop" class="small-btn">应用</button> </div> </div> <div class="property-group"> <label>音量</label> <input type="range" id="volume-slider" min="0" max="200" value="100"> <span id="volume-value">100%</span> </div> <div class="property-group"> <label>滤镜</label> <select id="filter-select"> <option value="none">无滤镜</option> <option value="grayscale">灰度</option> <option value="sepia">怀旧</option> <option value="invert">反色</option> <option value="brightness">亮度增强</option> </select> </div> <div class="property-group"> <label>添加文字</label> <input type="text" id="text-input" placeholder="输入文字"> <div class="text-controls"> <input type="color" id="text-color" value="#FFFFFF"> <input type="number" id="text-size" min="10" max="100" value="24"> <button id="btn-add-text" class="small-btn">添加</button> </div> </div> </div> </div> </div> <!-- 底部时间轴 --> <div class="editor-timeline"> <div class="timeline-header"> <div class="track-labels"> <div class="track-label">视频轨道</div> <div class="track-label">音频轨道</div> <div class="track-label">文字轨道</div> </div> </div> <div class="timeline-body"> <div class="timeline-tracks"> <!-- 动态生成轨道 --> </div> <div class="timeline-ruler"> <!-- 时间刻度 --> </div> </div> </div> </div> 3.2 实现核心JavaScript编辑器类 创建编辑器的主要JavaScript逻辑: // 在插件目录中创建assets/js/video-editor-core.js class VideoEditor { constructor(config) { this.config = { containerId: 'video-editor-app', maxFileSize: 500 * 1024 * 1024, // 500MB allowedFormats: ['video/mp4', 'video/webm', 'video/ogg'], ...config }; this.state = { currentProject: null, mediaElements: [], timelineElements: [], isPlaying: false, currentTime: 0, duration: 0 }; this.init(); } async init() { // 初始化DOM元素引用 this.container = document.getElementById(this.config.containerId); this.canvas = document.getElementById('video-canvas'); this.video = document.getElementById('source-video'); this.ctx = this.canvas.getContext('2d'); // 初始化FFmpeg.wasm await this.initFFmpeg(); // 绑定事件 this.bindEvents(); // 加载用户媒体库 await this.loadMediaLibrary(); // 初始化时间轴 this.initTimeline(); } async initFFmpeg() { // 检查浏览器是否支持WebAssembly if (!window.WebAssembly) { console.error('浏览器不支持WebAssembly,部分功能将受限'); return; } try { // 加载FFmpeg.wasm const { createFFmpeg, fetchFile } = FFmpeg; this.ffmpeg = createFFmpeg({ log: true }); // 显示加载状态 this.showMessage('正在加载视频处理引擎...', 'info'); await this.ffmpeg.load(); this.showMessage('视频编辑器准备就绪', 'success'); } catch (error) { console.error('FFmpeg初始化失败:', error); this.showMessage('视频处理引擎加载失败,基础编辑功能仍可用', 'warning'); } } bindEvents() { // 播放控制 document.getElementById('btn-play').addEventListener('click', () => this.togglePlay()); // 文件上传 document.getElementById('btn-upload-media').addEventListener('click', () => this.openFileUpload()); // 时间轴拖动 this.setupTimelineEvents(); // 属性控制 document.getElementById('volume-slider').addEventListener('input', (e) => { this.setVolume(e.target.value / 100); document.getElementById('volume-value').textContent = `${e.target.value}%`; }); document.getElementById('filter-select').addEventListener('change', (e) => { this.applyFilter(e.target.value); }); // 文字添加 document.getElementById('btn-add-text').addEventListener('click', () => { const text = document.getElementById('text-input').value; const color = document.getElementById('text-color').value; const size = document.getElementById('text-size').value; if (text.trim()) { this.addTextElement(text, color, parseInt(size)); document.getElementById('text-input').value = ''; } }); // 裁剪应用 document.getElementById('btn-apply-crop').addEventListener('click', () => { const start = parseFloat(document.getElementById('crop-start').value) || 0; const end = parseFloat(document.getElementById('crop-end').value) || this.state.duration; if (end > start) { this.cropVideo(start, end); } }); } async openFileUpload() { // 创建文件输入元素 const input = document.createElement('input'); input.type = 'file'; input.accept = this.config.allowedFormats.join(','); input.multiple = true; input.onchange = async (e) => { const files = Array.from(e.target.files); for (const file of files) { // 检查文件大小 if (file.size > this.config.maxFileSize) { this.showMessage(`文件 ${file.name} 超过大小限制`, 'error'); continue; } // 检查文件类型 if (!this.config.allowedFormats.includes(file.type)) { this.showMessage(`文件 ${file.name} 格式不支持`, 'error'); continue; } // 上传文件 await this.uploadMediaFile(file); } }; input.click(); } async uploadMediaFile(file) { // 创建FormData const formData = new FormData(); formData.append('action', 'video_editor_upload'); formData.append('file', file); formData.append('nonce', this.config.nonce); try { this.showMessage(`正在上传 ${file.name}...`, 'info'); const response = await fetch(this.config.ajaxUrl, { method: 'POST', body: formData }); const result = await response.json(); if (result.success) { this.showMessage(`${file.name} 上传成功`, 'success'); this.addMediaToLibrary(result.data); } else { this.showMessage(`上传失败: ${result.data.message}`, 'error'); } } catch (error) { console.error('上传失败:', error); this.showMessage('上传失败,请检查网络连接', 'error'); } } addMediaToLibrary(mediaData) { // 创建媒体库项目 const mediaItem = { id: mediaData.id, type: mediaData.type, url: mediaData.url, thumbnail: mediaData.thumbnail || mediaData.url, duration: mediaData.duration || 0, name: mediaData.name }; this.state.mediaElements.push(mediaItem); // 更新媒体库UI this.renderMediaLibrary(); // 如果这是第一个视频,自动加载到编辑器 if (mediaData.type.startsWith('video/') && !this.state.currentProject) { this.loadVideo(mediaItem); } } async loadVideo(mediaItem) { this.showMessage(`正在加载视频: ${mediaItem.name}`, 'info'); // 设置视频源 this.video.src = mediaItem.url; // 等待视频元数据加载 await new Promise((resolve) => { this.video.onloadedmetadata = () => { this.state.duration = this.video.duration; this.updateDurationDisplay(); resolve(); }; }); // 初始化项目 this.state.currentProject = { id: Date.now().toString(), name: '未命名项目', sourceVideo: mediaItem, edits: [], elements: [] }; // 开始渲染循环 this.startRenderLoop(); this.showMessage('视频加载完成,可以开始编辑', 'success'); } startRenderLoop() { const renderFrame = () => { if (!this.state.isPlaying && this.state.currentTime === this.video.currentTime) { requestAnimationFrame(renderFrame); return; } this.state.currentTime = this.video.currentTime; this.updateTimeDisplay(); this.updateTimelinePosition(); // 清除画布 this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); // 绘制视频帧 this.ctx.drawImage( this.video, 0, 0, this.video.videoWidth, this.video.videoHeight, 0, 0, this.canvas.width, this.canvas.height ); // 应用当前滤镜 this.applyCurrentFilter(); // 绘制叠加元素(文字、图形等) this.renderOverlayElements(); requestAnimationFrame(renderFrame); }; renderFrame(); } applyCurrentFilter() { const filter = document.getElementById('filter-select').value; switch(filter) { case 'grayscale': this.ctx.filter = 'grayscale(100%)'; this.ctx.drawImage(this.canvas, 0, 0); this.ctx.filter = 'none'; break; case 'sepia': this.ctx.filter = 'sepia(100%)'; this.ctx.drawImage(this.canvas, 0, 0); this.ctx.filter = 'none'; break; case 'invert': this.ctx.filter = 'invert(100%)'; this.ctx.drawImage(this.canvas, 0, 0); this.ctx.filter = 'none'; break; case 'brightness': this.ctx.filter = 'brightness(150%)'; this.ctx.drawImage(this.canvas, 0, 0); this.ctx.filter = 'none'; break; } } addTextElement(text, color, size) { const textElement = { id: `text_${Date.now()}`, type: 'text', content: text, color: color, size: size, position: { x: 50, y: 50 }, startTime: this.state.currentTime, duration: 5, // 显示5秒 font: 'Arial' }; this.state.currentProject.elements.push(textElement); this.addToTimeline(textElement); } renderOverlayElements() { const currentTime = this.state.currentTime; this.state.currentProject.elements.forEach(element => { if (currentTime >= element.startTime && currentTime <= element.startTime + element.duration) { if (element.type === 'text') { this.ctx.fillStyle = element.color; this.ctx.font = `${element.size}px ${element.font}`; this.ctx.fillText(element.content, element.position.x, element.position.y); } } }); } async cropVideo(startTime, endTime) { if (!this.ffmpeg) { this.showMessage('视频裁剪需要FFmpeg支持,请稍后再试', 'warning'); return; } this.showMessage('正在裁剪视频...', 'info'); try { // 获取视频文件 const response = await fetch(this.state.currentProject.sourceVideo.url); const videoBlob = await response.blob(); // 写入FFmpeg文件系统 this.ffmpeg.FS('writeFile', 'input.mp4', await fetchFile(videoBlob)); // 执行裁剪命令 await this.ffmpeg.run( '-i', 'input.mp4', '-ss', startTime.toString(), '-to', endTime.toString(), '-c', 'copy', 'output.mp4' ); // 读取结果 const data = this.ffmpeg.FS('readFile', 'output.mp4'); const croppedBlob = new Blob([data.buffer], { type: 'video/mp4' }); // 创建新视频元素 const croppedUrl = URL.createObjectURL(croppedBlob); const croppedVideo = { id: `cropped_${Date.now()}`, type: 'video/mp4', url: croppedUrl, name: `${this.state.currentProject.sourceVideo.name}_裁剪版`, duration: endTime - startTime }; // 添加到媒体库 this.addMediaToLibrary(croppedVideo); // 加载裁剪后的视频 this.loadVideo(croppedVideo); this.showMessage('视频裁剪完成', 'success'); } catch (error) { console.error('裁剪失败:', error); this.showMessage('视频裁剪失败', 'error'); } } // 其他辅助方法 togglePlay() { if (this.state.isPlaying) { this.video.pause(); } else { this.video.play(); } this.state.isPlaying = !this.state.isPlaying; const playBtn = document.getElementById('btn-play'); playBtn.innerHTML = this.state.isPlaying ? '<i class="icon-pause"></i>' : '<i class="icon-play"></i>'; } updateTimeDisplay() { document.getElementById('current-time').textContent = this.formatTime(this.state.currentTime); } updateDurationDisplay() { document.getElementById('duration').textContent = this.formatTime(this.state.duration); } formatTime(seconds) { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } showMessage(message, type = 'info') { // 创建消息元素 const messageEl = document.createElement('div'); messageEl.className = `editor-message editor-message-${type}`; messageEl.textContent = message; // 添加到容器 this.container.appendChild(messageEl); // 3秒后移除 setTimeout(() => { if (messageEl.parentNode) { messageEl.parentNode.removeChild(messageEl); } }, 3000); } // 初始化时间轴 initTimeline() { // 创建时间刻度 this.renderTimelineRuler(); // 设置拖动事件 this.setupTimelineEvents(); } renderTimelineRuler() { const ruler = document.querySelector('.timeline-ruler'); if (!ruler) return; const totalSeconds = Math.ceil(this.state.duration); const pixelsPerSecond = 50; // 每秒钟50像素 for (let i = 0; i <= totalSeconds; i += 5) { const tick = document.createElement('div'); tick.className = 'timeline-tick'; tick.style.left = `${i * pixelsPerSecond}px`; const label = document.createElement('span'); label.className = 'timeline-label'; label.textContent = this.formatTime(i); tick.appendChild(label); ruler.appendChild(tick); } } setupTimelineEvents() { const timeline = document.querySelector('.timeline-track'); if (!timeline) return; timeline.addEventListener('click', (e) => { const rect = timeline.getBoundingClientRect(); const clickX = e.clientX - rect.left; const pixelsPerSecond = 50; const time = clickX / pixelsPerSecond; this.video.currentTime = Math.min(time, this.state.duration); this.state.currentTime = this.video.currentTime; }); } updateTimelinePosition() { const scrubber = document.querySelector('.timeline-scrubber'); if (!scrubber) return; const progress = (this.state.currentTime / this.state.duration) * 100; scrubber.style.left = `${progress}%`; } addToTimeline(element) { const timelineTracks = document.querySelector('.timeline-tracks'); if (!timelineTracks) return; const track = document.createElement('div'); track.className = 'timeline-element'; track.dataset.id = element.id; // 计算位置和宽度 const pixelsPerSecond = 50; const left = element.startTime * pixelsPerSecond; const width = element.duration * pixelsPerSecond; track.style.left = `${left}px`; track.style.width = `${width}px`; // 根据类型设置样式 if (element.type === 'text') { track.classList.add('text-element'); track.innerHTML = `<span class="element-label">T</span>`; } timelineTracks.appendChild(track); } renderMediaLibrary() { const mediaLibrary = document.querySelector('.media-library'); if (!mediaLibrary) return; mediaLibrary.innerHTML = ''; this.state.mediaElements.forEach(media => { const item = document.createElement('div'); item.className = 'media-item'; item.dataset.id = media.id; item.innerHTML = ` <div class="media-thumbnail"> ${media.type.startsWith('video/') ? `<i class="icon-video"></i>` : `<i class="icon-audio"></i>`} </div> <div class="media-info"> <div class="media-name">${media.name}</div> ${media.duration ? `<div class="media-duration">${this.formatTime(media.duration)}</div>` : ''} </div> `; item.addEventListener('click', () => { if (media.type.startsWith('video/')) { this.loadVideo(media); } else if (media.type.startsWith('audio/')) { this.addAudioToProject(media); } }); mediaLibrary.appendChild(item); }); } } // 初始化编辑器document.addEventListener('DOMContentLoaded', () => { window.videoEditor = new VideoEditor({ ajaxUrl: videoEditorConfig.ajaxUrl, nonce: videoEditorConfig.nonce, userId: videoEditorConfig.userId }); }); ## 第四部分:后端API与数据处理 ### 4.1 实现REST API端点 扩展WordPress插件类,添加REST API支持: public function register_rest_routes() { // 项目管理端点 register_rest_route('video-editor/v1', '/projects', array( array( 'methods' => 'GET', 'callback' => array($this, 'get_user_projects'), 'permission_callback' => array($this, 'check_user_permission'), ), array( 'methods' => 'POST', 'callback' => array($this, 'create_project'), 'permission_callback' => array($this, 'check_user_permission'), ), )); register_rest_route('video-editor/v1', '/projects/(?P<id>d+)', array( array( 'methods' => 'GET', 'callback' => array($this, 'get_project'), 'permission_callback' => array($this, 'check_user_permission'), ), array( 'methods' => 'PUT', 'callback' => array($this, 'update_project'), 'permission_callback' => array($this, 'check_user_permission'), ), array( 'methods' => 'DELETE', 'callback' => array($this, 'delete_project'), 'permission_callback' => array($this, 'check_user_permission'), ), )); // 文件上传端点 register_rest_route('video-editor/v1', '/upload', array( 'methods' => 'POST', 'callback' => array($this, 'handle_file_upload'), 'permission_callback' => array($this, 'check_user_permission'), )); // 视频处理端点 register_rest_route('video-editor/v1', '/process', array( 'methods' => 'POST', 'callback' => array($this, 'process_video'), 'permission_callback' => array($this, 'check_user_permission'), )); } public function check_user_permission($request) { return is_user_logged_in(); } public function handle_file_upload($request) { // 检查文件上传 if (empty($_FILES['file'])) { return new WP_Error('no_file', '没有上传文件', array('status' => 400)); } $file = $_FILES['file']; // 检查文件类型 $allowed_types = get_option('video_editor_allowed_formats', 'mp4,webm,mov,avi'); $allowed_types = explode(',', $allowed_types); $file_ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); if (!in_array($file_ext, $allowed_types)) { return new WP_Error('invalid_type', '不支持的文件格式', array('status' => 400)); } // 检查文件大小 $max_size = get_option('video_editor_max_upload_size', 500) * 1024 * 1024; if ($file['size'] > $max_size) { return new WP_Error('file_too_large', '文件太大', array('status' => 400)); } // 处理上传 require_once(ABSPATH . 'wp-admin/includes/file.php'); require_once(ABSPATH . 'wp-admin/includes/media.php'); require_once(ABSPATH . 'wp-admin/includes/image.php'); $upload_overrides = array('test_form' => false); $uploaded_file = wp_handle_upload($file, $upload_overrides); if (isset($uploaded_file['error'])) { return new WP_Error('upload_error', $uploaded_file['error'], array('status' => 500)); } // 创建附件 $attachment = array( 'post_mime_type' => $uploaded_file['type'], 'post_title' => preg_replace('/.[^.]+$/', '', basename($uploaded_file['file'])), 'post_content' => '', 'post_status' => 'inherit', 'guid' => $uploaded_file['url'] ); $attach_id = wp_insert_attachment($attachment, $uploaded_file['file']); // 生成元数据 $attach_data = wp_generate_attachment_metadata($attach_id, $uploaded_file['file']); wp_update_attachment_metadata($attach_id, $attach_data); // 获取视频时长(如果可能) $duration = 0; if (strpos($uploaded_file['type'], 'video/') === 0) { $duration = $this->get_video_duration($uploaded_file['file']); } // 生成缩略图 $thumbnail_url = ''; if (strpos($uploaded_file['type'], 'video/') === 0) { $thumbnail_url = $this->generate_video_thumbnail($attach_id, $uploaded_file['file']); } return rest_ensure_response(array( 'success' => true, 'data' => array( 'id' => $attach_id, 'url' => $uploaded_file['url'], 'type' => $uploaded_file['type'], 'name' => basename($uploaded_file['file']), 'duration' => $duration, 'thumbnail' => $thumbnail_url ) )); } private function get_video_duration($file_path) { // 使用FFmpeg获取视频时长 if (function_exists('shell_exec')) { $cmd = "ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 " . escapeshellarg($file_path); $duration = shell_exec($cmd); return floatval($duration); } return 0; } private function generate_video_thumbnail($attachment_id, $file_path) { // 使用FFmpeg生成缩略图 $upload_dir = wp_upload_dir(); $thumbnail_path = $upload_dir['path'] . '/thumb_' . $attachment_id . '.jpg'; if (function_exists('shell_exec')) { $cmd = "ffmpeg -i " . escapeshellarg($file_path) . " -ss 00:00:01 -vframes 1 -q:v 2 " . escapeshellarg($thumbnail_path) . " 2>&1"; shell_exec($cmd); if (file_exists($thumbnail_path)) { // 将缩略图添加到媒体库 $thumbnail_attachment = array( 'post_mime_type' => 'image/jpeg', 'post_title'
发表评论WordPress高级教程:开发集成在线问卷调研与可视化数据报告生成器 引言:WordPress作为企业级应用开发平台 WordPress早已超越了简单的博客系统范畴,成为功能强大的内容管理系统(CMS)和应用程序开发平台。全球超过43%的网站基于WordPress构建,其强大的插件架构和灵活的代码结构使其成为开发各种互联网小工具的理想选择。本教程将深入探讨如何通过WordPress代码二次开发,实现一个集在线问卷调研与可视化数据报告生成器于一体的高级功能模块。 在当今数据驱动的商业环境中,在线问卷调研和数据可视化分析已成为企业决策、市场研究和用户反馈收集的重要工具。通过将这些功能集成到WordPress网站中,我们可以为用户提供无缝的体验,同时利用WordPress强大的用户管理、权限控制和内容展示能力。 第一部分:项目架构设计与技术选型 1.1 系统需求分析 在开始开发之前,我们需要明确系统的核心需求: 问卷创建与管理:支持多种题型(单选、多选、文本输入、评分等) 问卷发布与收集:可通过短代码、小工具或独立页面嵌入 响应数据存储:高效存储和检索大量问卷响应数据 数据可视化:自动生成图表和报告 权限控制:基于WordPress角色系统的访问控制 数据导出:支持CSV、Excel和PDF格式导出 1.2 技术架构设计 我们将采用分层架构设计: 表现层 (Presentation Layer) ├── WordPress前端界面 ├── 管理后台界面 └── 可视化报告界面 业务逻辑层 (Business Logic Layer) ├── 问卷管理模块 ├── 响应处理模块 ├── 数据分析模块 └── 报告生成模块 数据访问层 (Data Access Layer) ├── WordPress自定义表 ├── 选项API (Options API) └── 文件系统(图表缓存) 基础服务层 (Infrastructure Layer) ├── WordPress核心API ├── 图表库(Chart.js或ECharts) └── PDF生成库 1.3 技术栈选择 核心框架:WordPress 5.8+ 前端图表库:Chart.js 3.x(轻量级且功能强大) PDF生成:TCPDF或Dompdf 数据导出:PhpSpreadsheet(用于Excel导出) AJAX处理:WordPress REST API + jQuery/Axios 数据库:MySQL 5.6+(与WordPress兼容) 第二部分:数据库设计与自定义表创建 2.1 自定义表设计 虽然WordPress提供了Posts和Post Meta表,但为了问卷数据的性能优化和规范化,我们将创建自定义表: -- 问卷主表 CREATE TABLE wp_surveys ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, title VARCHAR(255) NOT NULL, description TEXT, status ENUM('draft', 'published', 'closed') DEFAULT 'draft', settings LONGTEXT, -- JSON格式存储配置 created_by BIGINT(20) UNSIGNED, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY status_idx (status), KEY created_by_idx (created_by) ); -- 问卷问题表 CREATE TABLE wp_survey_questions ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, survey_id BIGINT(20) UNSIGNED NOT NULL, question_text TEXT NOT NULL, question_type ENUM('single_choice', 'multiple_choice', 'text', 'rating', 'likert') NOT NULL, options LONGTEXT, -- JSON格式存储选项 is_required BOOLEAN DEFAULT FALSE, sort_order INT(11) DEFAULT 0, settings LONGTEXT, PRIMARY KEY (id), KEY survey_id_idx (survey_id), FOREIGN KEY (survey_id) REFERENCES wp_surveys(id) ON DELETE CASCADE ); -- 问卷响应表 CREATE TABLE wp_survey_responses ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, survey_id BIGINT(20) UNSIGNED NOT NULL, respondent_ip VARCHAR(45), respondent_user_id BIGINT(20) UNSIGNED, started_at DATETIME, completed_at DATETIME, time_spent INT(11), -- 单位:秒 metadata LONGTEXT, PRIMARY KEY (id), KEY survey_id_idx (survey_id), KEY respondent_user_id_idx (respondent_user_id), FOREIGN KEY (survey_id) REFERENCES wp_surveys(id) ON DELETE CASCADE ); -- 响应答案表 CREATE TABLE wp_survey_answers ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, response_id BIGINT(20) UNSIGNED NOT NULL, question_id BIGINT(20) UNSIGNED NOT NULL, answer_text TEXT, answer_value TEXT, PRIMARY KEY (id), KEY response_id_idx (response_id), KEY question_id_idx (question_id), FOREIGN KEY (response_id) REFERENCES wp_survey_responses(id) ON DELETE CASCADE, FOREIGN KEY (question_id) REFERENCES wp_survey_questions(id) ON DELETE CASCADE ); 2.2 数据库操作类实现 创建数据库操作类,封装所有数据库交互逻辑: <?php /** * 问卷系统数据库操作类 */ class Survey_DB_Manager { private static $instance = null; private $wpdb; private $charset_collate; private $table_prefix; // 表名常量 const TABLE_SURVEYS = 'surveys'; const TABLE_QUESTIONS = 'survey_questions'; const TABLE_RESPONSES = 'survey_responses'; const TABLE_ANSWERS = 'survey_answers'; private function __construct() { global $wpdb; $this->wpdb = $wpdb; $this->table_prefix = $wpdb->prefix . 'survey_'; $this->charset_collate = $wpdb->get_charset_collate(); } public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } /** * 创建或更新数据库表 */ public function create_tables() { require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); $sql = array(); // 创建问卷主表 $sql[] = "CREATE TABLE {$this->table_prefix}surveys ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, title VARCHAR(255) NOT NULL, description TEXT, status ENUM('draft', 'published', 'closed') DEFAULT 'draft', settings LONGTEXT, created_by BIGINT(20) UNSIGNED, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY status_idx (status), KEY created_by_idx (created_by) ) {$this->charset_collate};"; // 创建其他表的SQL语句... // 执行所有SQL foreach ($sql as $query) { dbDelta($query); } // 更新数据库版本 update_option('survey_db_version', '1.0.0'); } /** * 获取问卷列表 */ public function get_surveys($args = array()) { $defaults = array( 'status' => 'published', 'per_page' => 10, 'page' => 1, 'orderby' => 'created_at', 'order' => 'DESC' ); $args = wp_parse_args($args, $defaults); $offset = ($args['page'] - 1) * $args['per_page']; $where = array('1=1'); $prepare_values = array(); if (!empty($args['status'])) { $where[] = 'status = %s'; $prepare_values[] = $args['status']; } if (!empty($args['created_by'])) { $where[] = 'created_by = %d'; $prepare_values[] = $args['created_by']; } $where_clause = implode(' AND ', $where); $query = "SELECT * FROM {$this->table_prefix}surveys WHERE {$where_clause} ORDER BY {$args['orderby']} {$args['order']} LIMIT %d OFFSET %d"; $prepare_values[] = $args['per_page']; $prepare_values[] = $offset; return $this->wpdb->get_results( $this->wpdb->prepare($query, $prepare_values) ); } /** * 保存问卷响应 */ public function save_response($survey_id, $answers, $user_id = null) { $this->wpdb->query('START TRANSACTION'); try { // 插入响应记录 $response_data = array( 'survey_id' => $survey_id, 'respondent_user_id' => $user_id, 'respondent_ip' => $this->get_client_ip(), 'started_at' => current_time('mysql'), 'completed_at' => current_time('mysql'), 'metadata' => json_encode(array( 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'referer' => $_SERVER['HTTP_REFERER'] ?? '' )) ); $this->wpdb->insert( $this->table_prefix . 'responses', $response_data ); $response_id = $this->wpdb->insert_id; // 保存每个问题的答案 foreach ($answers as $question_id => $answer) { $answer_data = array( 'response_id' => $response_id, 'question_id' => $question_id, 'answer_text' => is_array($answer) ? json_encode($answer) : $answer, 'answer_value' => is_array($answer) ? implode(',', $answer) : $answer ); $this->wpdb->insert( $this->table_prefix . 'answers', $answer_data ); } $this->wpdb->query('COMMIT'); return $response_id; } catch (Exception $e) { $this->wpdb->query('ROLLBACK'); error_log('保存问卷响应失败: ' . $e->getMessage()); return false; } } /** * 获取客户端IP地址 */ private function get_client_ip() { $ip_keys = array('HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR'); foreach ($ip_keys as $key) { if (array_key_exists($key, $_SERVER) === true) { foreach (explode(',', $_SERVER[$key]) as $ip) { $ip = trim($ip); if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) { return $ip; } } } } return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; } /** * 获取问卷统计数据 */ public function get_survey_stats($survey_id) { $stats = array(); // 获取响应总数 $stats['total_responses'] = (int) $this->wpdb->get_var( $this->wpdb->prepare( "SELECT COUNT(*) FROM {$this->table_prefix}responses WHERE survey_id = %d", $survey_id ) ); // 获取最近7天的响应趋势 $stats['response_trend'] = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT DATE(completed_at) as date, COUNT(*) as count FROM {$this->table_prefix}responses WHERE survey_id = %d AND completed_at >= DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY DATE(completed_at) ORDER BY date ASC", $survey_id ) ); return $stats; } } ?> 第三部分:问卷系统核心功能开发 3.1 问卷创建与管理模块 创建问卷管理类,处理问卷的CRUD操作: <?php /** * 问卷管理类 */ class Survey_Manager { private $db; public function __construct() { $this->db = Survey_DB_Manager::get_instance(); } /** * 创建新问卷 */ public function create_survey($data) { $defaults = array( 'title' => '新问卷', 'description' => '', 'status' => 'draft', 'settings' => array( 'allow_multiple_responses' => false, 'require_login' => false, 'show_progress' => true, 'thank_you_message' => '感谢您的参与!' ) ); $data = wp_parse_args($data, $defaults); // 验证数据 if (empty($data['title'])) { return new WP_Error('empty_title', '问卷标题不能为空'); } // 获取当前用户ID $current_user_id = get_current_user_id(); $survey_data = array( 'title' => sanitize_text_field($data['title']), 'description' => wp_kses_post($data['description']), 'status' => in_array($data['status'], array('draft', 'published', 'closed')) ? $data['status'] : 'draft', 'settings' => json_encode($data['settings']), 'created_by' => $current_user_id ); // 插入数据库 global $wpdb; $table_name = $wpdb->prefix . 'survey_surveys'; $result = $wpdb->insert($table_name, $survey_data); if ($result === false) { return new WP_Error('db_error', '创建问卷失败'); } $survey_id = $wpdb->insert_id; // 记录操作日志 $this->log_activity($survey_id, 'create', '创建问卷'); return $survey_id; } /** * 添加问题到问卷 */ public function add_question($survey_id, $question_data) { // 验证问卷是否存在 if (!$this->survey_exists($survey_id)) { return new WP_Error('not_found', '问卷不存在'); } $defaults = array( 'question_text' => '', 'question_type' => 'single_choice', 'options' => array(), 'is_required' => false, 'settings' => array() ); $question_data = wp_parse_args($question_data, $defaults); // 根据问题类型验证选项 $validation_result = $this->validate_question($question_data); if (is_wp_error($validation_result)) { return $validation_result; } // 获取排序值 $sort_order = $this->get_next_sort_order($survey_id); $question = array( 'survey_id' => $survey_id, 'question_text' => sanitize_text_field($question_data['question_text']), 'question_type' => $question_data['question_type'], 'options' => json_encode($question_data['options']), 'is_required' => (bool) $question_data['is_required'], 'sort_order' => $sort_order, 'settings' => json_encode($question_data['settings']) ); global $wpdb; $table_name = $wpdb->prefix . 'survey_questions'; $result = $wpdb->insert($table_name, $question); if ($result === false) { return new WP_Error('db_error', '添加问题失败'); } $question_id = $wpdb->insert_id; // 记录操作日志 $this->log_activity($survey_id, 'add_question', '添加问题: ' . $question_data['question_text']); return $question_id; } /** * 验证问题数据 */ private function validate_question($question_data) { if (empty($question_data['question_text'])) { return new WP_Error('empty_question', '问题内容不能为空'); } $allowed_types = array('single_choice', 'multiple_choice', 'text', 'rating', 'likert'); if (!in_array($question_data['question_type'], $allowed_types)) { return new WP_Error('invalid_type', '无效的问题类型'); } // 对于选择题,验证选项 if (in_array($question_data['question_type'], array('single_choice', 'multiple_choice'))) { if (empty($question_data['options']) || !is_array($question_data['options'])) { return new WP_Error('empty_options', '选择题必须提供选项'); } // 验证每个选项 foreach ($question_data['options'] as $option) { if (empty(trim($option['text']))) { return new WP_Error('empty_option_text', '选项文本不能为空'); } } } return true; } /** * 获取下一个排序值 */ private function get_next_sort_order($survey_id) { global $wpdb; $table_name = $wpdb->prefix . 'survey_questions'; $max_sort = $wpdb->get_var( $wpdb->prepare( 第三部分:问卷系统核心功能开发(续) 3.2 问卷前端展示与提交处理 创建问卷前端渲染和提交处理类: <?php /** * 问卷前端展示类 */ class Survey_Frontend { private $db; private $survey_manager; public function __construct() { $this->db = Survey_DB_Manager::get_instance(); $this->survey_manager = new Survey_Manager(); add_shortcode('survey', array($this, 'render_survey_shortcode')); add_action('wp_enqueue_scripts', array($this, 'enqueue_frontend_scripts')); add_action('wp_ajax_submit_survey', array($this, 'handle_survey_submission')); add_action('wp_ajax_nopriv_submit_survey', array($this, 'handle_survey_submission')); } /** * 注册前端脚本和样式 */ public function enqueue_frontend_scripts() { // 问卷样式 wp_enqueue_style( 'survey-frontend-style', plugin_dir_url(__FILE__) . 'assets/css/survey-frontend.css', array(), '1.0.0' ); // 问卷脚本 wp_enqueue_script( 'survey-frontend-script', plugin_dir_url(__FILE__) . 'assets/js/survey-frontend.js', array('jquery'), '1.0.0', true ); // 本地化脚本 wp_localize_script('survey-frontend-script', 'survey_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('survey_nonce') )); } /** * 渲染问卷短代码 */ public function render_survey_shortcode($atts) { $atts = shortcode_atts(array( 'id' => 0, 'title' => true, 'description' => true ), $atts, 'survey'); $survey_id = intval($atts['id']); if (!$survey_id) { return '<div class="survey-error">请指定问卷ID</div>'; } // 获取问卷数据 $survey = $this->get_survey($survey_id); if (!$survey) { return '<div class="survey-error">问卷不存在或已被删除</div>'; } // 检查问卷状态 if ($survey->status !== 'published') { return '<div class="survey-error">该问卷当前不可用</div>'; } // 检查用户权限 $settings = json_decode($survey->settings, true); if ($settings['require_login'] && !is_user_logged_in()) { return '<div class="survey-login-required">请先登录以参与此问卷</div>'; } // 检查是否允许重复提交 if (!$settings['allow_multiple_responses'] && $this->has_user_responded($survey_id)) { return '<div class="survey-already-responded">您已经参与过此问卷,感谢您的参与!</div>'; } // 获取问卷问题 $questions = $this->get_survey_questions($survey_id); // 渲染问卷HTML ob_start(); ?> <div class="survey-container" data-survey-id="<?php echo esc_attr($survey_id); ?>"> <?php if ($atts['title']): ?> <div class="survey-header"> <h2 class="survey-title"><?php echo esc_html($survey->title); ?></h2> <?php if ($atts['description'] && !empty($survey->description)): ?> <div class="survey-description"> <?php echo wp_kses_post($survey->description); ?> </div> <?php endif; ?> </div> <?php endif; ?> <form id="survey-form-<?php echo esc_attr($survey_id); ?>" class="survey-form"> <div class="survey-questions"> <?php foreach ($questions as $index => $question): ?> <div class="survey-question" data-question-id="<?php echo esc_attr($question->id); ?>" data-question-type="<?php echo esc_attr($question->question_type); ?>"> <div class="question-header"> <h3 class="question-title"> <?php echo ($index + 1) . '. ' . esc_html($question->question_text); ?> <?php if ($question->is_required): ?> <span class="required-indicator">*</span> <?php endif; ?> </h3> </div> <div class="question-body"> <?php echo $this->render_question_field($question); ?> </div> <?php if ($settings['show_progress']): ?> <div class="question-progress"> <div class="progress-bar"> <div class="progress-fill" style="width: <?php echo (($index + 1) / count($questions)) * 100; ?>%"></div> </div> <div class="progress-text"> 问题 <?php echo $index + 1; ?> / <?php echo count($questions); ?> </div> </div> <?php endif; ?> </div> <?php endforeach; ?> </div> <div class="survey-footer"> <div class="form-actions"> <button type="submit" class="survey-submit-btn"> <span class="btn-text">提交问卷</span> <span class="spinner" style="display: none;"></span> </button> <button type="button" class="survey-clear-btn">清除答案</button> </div> <div class="required-notice"> <span class="required-indicator">*</span> 表示必填问题 </div> </div> <div class="survey-messages" style="display: none;"></div> </form> <div class="survey-thank-you" style="display: none;"> <div class="thank-you-content"> <?php echo wp_kses_post($settings['thank_you_message']); ?> </div> </div> </div> <?php return ob_get_clean(); } /** * 渲染问题字段 */ private function render_question_field($question) { $options = json_decode($question->options, true); $html = ''; switch ($question->question_type) { case 'single_choice': $html .= '<div class="single-choice-options">'; foreach ($options as $option) { $html .= sprintf( '<label class="option-label"><input type="radio" name="question_%s" value="%s" %s> %s</label>', esc_attr($question->id), esc_attr($option['value']), $question->is_required ? 'required' : '', esc_html($option['text']) ); } $html .= '</div>'; break; case 'multiple_choice': $html .= '<div class="multiple-choice-options">'; foreach ($options as $option) { $html .= sprintf( '<label class="option-label"><input type="checkbox" name="question_%s[]" value="%s"> %s</label>', esc_attr($question->id), esc_attr($option['value']), esc_html($option['text']) ); } $html .= '</div>'; break; case 'text': $html .= sprintf( '<textarea name="question_%s" rows="4" %s placeholder="请输入您的回答..."></textarea>', esc_attr($question->id), $question->is_required ? 'required' : '' ); break; case 'rating': $max_rating = isset($options['max_rating']) ? intval($options['max_rating']) : 5; $html .= '<div class="rating-options">'; for ($i = 1; $i <= $max_rating; $i++) { $html .= sprintf( '<label class="rating-label"><input type="radio" name="question_%s" value="%d" %s> %d</label>', esc_attr($question->id), $i, $question->is_required ? 'required' : '', $i ); } $html .= '</div>'; break; case 'likert': $html .= '<table class="likert-table">'; $html .= '<thead><tr><th></th>'; foreach ($options['scale'] as $scale_item) { $html .= '<th>' . esc_html($scale_item) . '</th>'; } $html .= '</tr></thead><tbody>'; foreach ($options['items'] as $item) { $html .= '<tr>'; $html .= '<td class="likert-item">' . esc_html($item) . '</td>'; foreach ($options['scale'] as $index => $scale_item) { $html .= sprintf( '<td><input type="radio" name="question_%s[%s]" value="%d"></td>', esc_attr($question->id), esc_attr($item), $index ); } $html .= '</tr>'; } $html .= '</tbody></table>'; break; } return $html; } /** * 处理问卷提交 */ public function handle_survey_submission() { // 验证nonce if (!check_ajax_referer('survey_nonce', 'nonce', false)) { wp_die(json_encode(array( 'success' => false, 'message' => '安全验证失败' ))); } // 获取提交数据 $survey_id = intval($_POST['survey_id']); $answers = isset($_POST['answers']) ? $_POST['answers'] : array(); // 验证问卷 $survey = $this->get_survey($survey_id); if (!$survey || $survey->status !== 'published') { wp_die(json_encode(array( 'success' => false, 'message' => '问卷不存在或已关闭' ))); } // 验证必填问题 $questions = $this->get_survey_questions($survey_id); $errors = array(); foreach ($questions as $question) { if ($question->is_required) { $question_id = $question->id; if (!isset($answers[$question_id]) || empty($answers[$question_id])) { $errors[] = sprintf('问题 "%s" 是必填项', $question->question_text); } } } if (!empty($errors)) { wp_die(json_encode(array( 'success' => false, 'message' => implode('<br>', $errors) ))); } // 获取当前用户ID $user_id = is_user_logged_in() ? get_current_user_id() : null; // 保存响应 $response_id = $this->db->save_response($survey_id, $answers, $user_id); if ($response_id) { // 发送成功响应 wp_die(json_encode(array( 'success' => true, 'message' => '问卷提交成功', 'response_id' => $response_id ))); } else { wp_die(json_encode(array( 'success' => false, 'message' => '提交失败,请稍后重试' ))); } } /** * 检查用户是否已经参与过问卷 */ private function has_user_responded($survey_id) { if (!is_user_logged_in()) { return false; } $user_id = get_current_user_id(); global $wpdb; $table_name = $wpdb->prefix . 'survey_responses'; $count = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $table_name WHERE survey_id = %d AND respondent_user_id = %d", $survey_id, $user_id )); return $count > 0; } /** * 获取问卷数据 */ private function get_survey($survey_id) { global $wpdb; $table_name = $wpdb->prefix . 'survey_surveys'; return $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $survey_id )); } /** * 获取问卷问题 */ private function get_survey_questions($survey_id) { global $wpdb; $table_name = $wpdb->prefix . 'survey_questions'; return $wpdb->get_results($wpdb->prepare( "SELECT * FROM $table_name WHERE survey_id = %d ORDER BY sort_order ASC", $survey_id )); } } ?> 3.3 前端JavaScript交互 创建前端JavaScript文件处理表单交互: // assets/js/survey-frontend.js (function($) { 'use strict'; // 问卷表单处理 class SurveyForm { constructor(formElement) { this.form = formElement; this.surveyId = formElement.closest('.survey-container').dataset.surveyId; this.submitBtn = formElement.querySelector('.survey-submit-btn'); this.clearBtn = formElement.querySelector('.survey-clear-btn'); this.messagesContainer = formElement.querySelector('.survey-messages'); this.thankYouContainer = formElement.closest('.survey-container').querySelector('.survey-thank-you'); this.init(); } init() { // 绑定事件 this.form.addEventListener('submit', this.handleSubmit.bind(this)); if (this.clearBtn) { this.clearBtn.addEventListener('click', this.handleClear.bind(this)); } // 初始化验证 this.initValidation(); // 初始化进度跟踪 this.initProgressTracking(); } initValidation() { // 实时验证 const inputs = this.form.querySelectorAll('input, textarea, select'); inputs.forEach(input => { input.addEventListener('blur', this.validateField.bind(this)); input.addEventListener('change', this.validateField.bind(this)); }); } initProgressTracking() { const questions = this.form.querySelectorAll('.survey-question'); if (questions.length === 0) return; // 跟踪已回答的问题 const updateProgress = () => { let answeredCount = 0; questions.forEach(question => { const inputs = question.querySelectorAll('input, textarea, select'); let isAnswered = false; inputs.forEach(input => { if (input.type === 'checkbox' || input.type === 'radio') { if (input.checked) isAnswered = true; } else if (input.type === 'text' || input.tagName === 'TEXTAREA' || input.tagName === 'SELECT') { if (input.value.trim() !== '') isAnswered = true; } }); if (isAnswered) answeredCount++; }); // 更新进度条 const progressFill = this.form.querySelector('.progress-fill'); if (progressFill) { const progress = (answeredCount / questions.length) * 100; progressFill.style.width = progress + '%'; } }; // 监听所有输入变化 this.form.addEventListener('input', updateProgress); this.form.addEventListener('change', updateProgress); // 初始更新 updateProgress(); } validateField(event) { const field = event.target; const question = field.closest('.survey-question'); if (!question) return; const isRequired = question.querySelector('.required-indicator') !== null; if (isRequired) { let isValid = true; const questionId = question.dataset.questionId; const questionType = question.dataset.questionType; switch (questionType) { case 'single_choice': const radios = question.querySelectorAll('input[type="radio"]'); isValid = Array.from(radios).some(radio => radio.checked); break; case 'multiple_choice': const checkboxes = question.querySelectorAll('input[type="checkbox"]:checked'); isValid = checkboxes.length > 0; break; case 'text': isValid = field.value.trim() !== ''; break; case 'rating': const ratingRadios = question.querySelectorAll('input[type="radio"]'); isValid = Array.from(ratingRadios).some(radio => radio.checked); break; } if (!isValid) { question.classList.add('has-error'); } else { question.classList.remove('has-error'); } } } collectFormData() { const formData = new FormData(); formData.append('action', 'submit_survey'); formData.append('nonce', survey_ajax.nonce); formData.append('survey_id', this.surveyId); const answers = {}; const questions = this.form.querySelectorAll('.survey-question'); questions.forEach(question => { const questionId = question.dataset.questionId; const questionType = question.dataset.questionType; switch (questionType) { case 'single_choice': const selectedRadio = question.querySelector('input[type="radio"]:checked'); if (selectedRadio) { answers[questionId] = selectedRadio.value; } break; case 'multiple_choice': const selectedCheckboxes = question.querySelectorAll('input[type="checkbox"]:checked'); const checkboxValues = Array.from(selectedCheckboxes).map(cb => cb.value); answers[questionId] = checkboxValues; break; case 'text': const textarea = question.querySelector('textarea'); if (textarea) { answers[questionId] = textarea.value.trim(); } break;
发表评论一步步教你,在WordPress中添加网站深色模式切换与个性化主题定制器 引言:为什么深色模式与主题定制器如此重要 在当今数字时代,用户体验已成为网站成功的关键因素之一。随着用户对个性化体验需求的增长,以及深色模式在各大平台和应用中的普及,为WordPress网站添加深色模式切换和个性化主题定制功能已成为提升用户参与度和满意度的有效手段。 深色模式不仅能够减少眼睛疲劳,特别是在低光环境下,还能节省设备电量(对于OLED屏幕尤为明显)。同时,个性化主题定制器允许用户根据自己的偏好调整网站外观,从而创造更加个性化的浏览体验。 本文将详细介绍如何通过WordPress代码二次开发,实现深色模式切换功能和个性化主题定制器,让你的网站更具现代感和用户友好性。 第一部分:准备工作与环境搭建 1.1 开发环境要求 在开始之前,确保你具备以下条件: 一个本地或线上的WordPress安装(建议使用最新版本) 代码编辑器(如VS Code、Sublime Text等) 基础的HTML、CSS、JavaScript和PHP知识 对WordPress主题结构有基本了解 子主题(推荐)或自定义主题用于开发 1.2 创建子主题 为了避免直接修改父主题导致更新时丢失更改,我们强烈建议创建子主题: 在WordPress的wp-content/themes/目录下创建新文件夹,命名为my-custom-theme 在该文件夹中创建style.css文件,添加以下内容: /* Theme Name: My Custom Theme Template: parent-theme-folder-name Version: 1.0.0 Description: 子主题用于添加深色模式和主题定制器 */ 创建functions.php文件,用于添加自定义功能 1.3 理解WordPress主题定制器API WordPress提供了强大的主题定制器API(Customizer API),允许开发者创建直观的界面,让用户实时预览并修改主题设置。我们将利用这个API来构建个性化主题定制器。 第二部分:实现深色模式切换功能 2.1 深色模式的基本原理 深色模式的实现主要基于CSS变量和JavaScript切换。我们将: 定义两套颜色变量(浅色和深色) 通过JavaScript切换CSS类来改变颜色方案 使用本地存储保存用户偏好 2.2 创建CSS颜色变量 在子主题的style.css文件中添加以下代码: :root { /* 浅色主题变量 */ --primary-color: #3498db; --secondary-color: #2ecc71; --background-color: #ffffff; --text-color: #333333; --header-bg: #f8f9fa; --border-color: #e0e0e0; --card-bg: #ffffff; --shadow-color: rgba(0, 0, 0, 0.1); } [data-theme="dark"] { /* 深色主题变量 */ --primary-color: #5dade2; --secondary-color: #58d68d; --background-color: #121212; --text-color: #e0e0e0; --header-bg: #1e1e1e; --border-color: #333333; --card-bg: #1e1e1e; --shadow-color: rgba(0, 0, 0, 0.3); } /* 应用CSS变量到具体元素 */ body { background-color: var(--background-color); color: var(--text-color); transition: background-color 0.3s, color 0.3s; } header { background-color: var(--header-bg); } .card { background-color: var(--card-bg); border: 1px solid var(--border-color); box-shadow: 0 2px 5px var(--shadow-color); } a { color: var(--primary-color); } .button { background-color: var(--primary-color); color: white; } 2.3 添加深色模式切换按钮 在主题的合适位置(通常是页眉或页脚)添加切换按钮。在header.php或创建自定义模板部分添加: <button id="dark-mode-toggle" class="dark-mode-toggle" aria-label="切换深色模式"> <span class="light-icon">☀️</span> <span class="dark-icon">🌙</span> </button> 2.4 实现JavaScript切换功能 创建js/dark-mode.js文件并添加以下代码: document.addEventListener('DOMContentLoaded', function() { const toggleButton = document.getElementById('dark-mode-toggle'); const currentTheme = localStorage.getItem('theme') || 'light'; // 应用保存的主题 if (currentTheme === 'dark') { document.documentElement.setAttribute('data-theme', 'dark'); toggleButton.classList.add('active'); } // 切换主题 toggleButton.addEventListener('click', function() { let theme = 'light'; if (document.documentElement.getAttribute('data-theme') !== 'dark') { document.documentElement.setAttribute('data-theme', 'dark'); theme = 'dark'; this.classList.add('active'); } else { document.documentElement.removeAttribute('data-theme'); this.classList.remove('active'); } // 保存用户选择 localStorage.setItem('theme', theme); // 发送事件,以便其他脚本可以响应主题变化 document.dispatchEvent(new CustomEvent('themeChanged', { detail: { theme } })); }); // 检测系统主题偏好 const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)'); // 如果用户没有明确选择,则使用系统偏好 if (!localStorage.getItem('theme') && prefersDarkScheme.matches) { document.documentElement.setAttribute('data-theme', 'dark'); toggleButton.classList.add('active'); localStorage.setItem('theme', 'dark'); } // 监听系统主题变化 prefersDarkScheme.addEventListener('change', function(e) { if (!localStorage.getItem('theme')) { if (e.matches) { document.documentElement.setAttribute('data-theme', 'dark'); toggleButton.classList.add('active'); } else { document.documentElement.removeAttribute('data-theme'); toggleButton.classList.remove('active'); } } }); }); 2.5 在WordPress中注册脚本 在子主题的functions.php中添加: function enqueue_dark_mode_scripts() { // 注册深色模式脚本 wp_register_script( 'dark-mode-script', get_stylesheet_directory_uri() . '/js/dark-mode.js', array(), '1.0.0', true ); // 注册深色模式样式 wp_register_style( 'dark-mode-style', get_stylesheet_directory_uri() . '/css/dark-mode.css' ); // 排队脚本和样式 wp_enqueue_script('dark-mode-script'); wp_enqueue_style('dark-mode-style'); } add_action('wp_enqueue_scripts', 'enqueue_dark_mode_scripts'); 第三部分:构建个性化主题定制器 3.1 理解WordPress定制器结构 WordPress定制器由以下部分组成: 部分(Sections):定制器中的主要分组 设置(Settings):存储用户选择的选项 控件(Controls):用户交互的UI元素 预览(Preview):实时预览更改 3.2 创建基础定制器设置 在functions.php中添加以下代码来创建定制器设置: function my_custom_theme_customizer($wp_customize) { // 添加"主题颜色"部分 $wp_customize->add_section('theme_colors_section', array( 'title' => __('主题颜色', 'my-custom-theme'), 'priority' => 30, )); // 主色调设置 $wp_customize->add_setting('primary_color_setting', array( 'default' => '#3498db', 'transport' => 'postMessage', // 实时预览 'sanitize_callback' => 'sanitize_hex_color', )); // 主色调控件 $wp_customize->add_control(new WP_Customize_Color_Control( $wp_customize, 'primary_color_control', array( 'label' => __('主色调', 'my-custom-theme'), 'section' => 'theme_colors_section', 'settings' => 'primary_color_setting', ) )); // 背景颜色设置 $wp_customize->add_setting('background_color_setting', array( 'default' => '#ffffff', 'transport' => 'postMessage', 'sanitize_callback' => 'sanitize_hex_color', )); // 背景颜色控件 $wp_customize->add_control(new WP_Customize_Color_Control( $wp_customize, 'background_color_control', array( 'label' => __('背景颜色', 'my-custom-theme'), 'section' => 'theme_colors_section', 'settings' => 'background_color_setting', ) )); // 文字颜色设置 $wp_customize->add_setting('text_color_setting', array( 'default' => '#333333', 'transport' => 'postMessage', 'sanitize_callback' => 'sanitize_hex_color', )); // 文字颜色控件 $wp_customize->add_control(new WP_Customize_Color_Control( $wp_customize, 'text_color_control', array( 'label' => __('文字颜色', 'my-custom-theme'), 'section' => 'theme_colors_section', 'settings' => 'text_color_setting', ) )); // 添加"排版"部分 $wp_customize->add_section('typography_section', array( 'title' => __('排版设置', 'my-custom-theme'), 'priority' => 40, )); // 字体选择设置 $wp_customize->add_setting('font_family_setting', array( 'default' => 'Arial, sans-serif', 'transport' => 'postMessage', 'sanitize_callback' => 'sanitize_text_field', )); // 字体选择控件 $wp_customize->add_control('font_family_control', array( 'label' => __('字体家族', 'my-custom-theme'), 'section' => 'typography_section', 'settings' => 'font_family_setting', 'type' => 'select', 'choices' => array( 'Arial, sans-serif' => 'Arial', 'Georgia, serif' => 'Georgia', "'Times New Roman', serif" => 'Times New Roman', "'Courier New', monospace" => 'Courier New', "'Trebuchet MS', sans-serif" => 'Trebuchet MS', 'Verdana, sans-serif' => 'Verdana', ), )); // 基础字体大小设置 $wp_customize->add_setting('base_font_size_setting', array( 'default' => '16', 'transport' => 'postMessage', 'sanitize_callback' => 'absint', )); // 基础字体大小控件 $wp_customize->add_control('base_font_size_control', array( 'label' => __('基础字体大小 (px)', 'my-custom-theme'), 'section' => 'typography_section', 'settings' => 'base_font_size_setting', 'type' => 'range', 'input_attrs' => array( 'min' => 12, 'max' => 24, 'step' => 1, ), )); } add_action('customize_register', 'my_custom_theme_customizer'); 3.3 添加实时预览JavaScript 创建js/customizer-preview.js文件: (function($) { // 主色调实时预览 wp.customize('primary_color_setting', function(value) { value.bind(function(newval) { $('body').css('--primary-color', newval); }); }); // 背景颜色实时预览 wp.customize('background_color_setting', function(value) { value.bind(function(newval) { $('body').css('--background-color', newval); }); }); // 文字颜色实时预览 wp.customize('text_color_setting', function(value) { value.bind(function(newval) { $('body').css('--text-color', newval); }); }); // 字体家族实时预览 wp.customize('font_family_setting', function(value) { value.bind(function(newval) { $('body').css('font-family', newval); }); }); // 基础字体大小实时预览 wp.customize('base_font_size_setting', function(value) { value.bind(function(newval) { $('html').css('font-size', newval + 'px'); }); }); })(jQuery); 3.4 注册定制器预览脚本 在functions.php中添加: function enqueue_customizer_preview_scripts() { wp_enqueue_script( 'customizer-preview-script', get_stylesheet_directory_uri() . '/js/customizer-preview.js', array('jquery', 'customize-preview'), '1.0.0', true ); } add_action('customize_preview_init', 'enqueue_customizer_preview_scripts'); 3.5 应用定制器设置到前端 创建css/customizer-styles.php文件,动态生成CSS: <?php header('Content-type: text/css'); $primary_color = get_theme_mod('primary_color_setting', '#3498db'); $background_color = get_theme_mod('background_color_setting', '#ffffff'); $text_color = get_theme_mod('text_color_setting', '#333333'); $font_family = get_theme_mod('font_family_setting', 'Arial, sans-serif'); $base_font_size = get_theme_mod('base_font_size_setting', '16'); ?> :root { --custom-primary-color: <?php echo esc_attr($primary_color); ?>; --custom-background-color: <?php echo esc_attr($background_color); ?>; --custom-text-color: <?php echo esc_attr($text_color); ?>; } body { font-family: <?php echo esc_attr($font_family); ?>; font-size: <?php echo esc_attr($base_font_size); ?>px; background-color: var(--custom-background-color); color: var(--custom-text-color); } a, .primary-color { color: var(--custom-primary-color); } .button-primary { background-color: var(--custom-primary-color); } 在functions.php中注册这个动态样式表: function enqueue_dynamic_styles() { wp_enqueue_style( 'dynamic-theme-styles', get_stylesheet_directory_uri() . '/css/customizer-styles.php' ); } add_action('wp_enqueue_scripts', 'enqueue_dynamic_styles'); 第四部分:集成常用互联网小工具 4.1 添加社交分享按钮 在functions.php中添加社交分享功能: // 社交分享按钮 function add_social_share_buttons($content) { if (is_single()) { $post_url = urlencode(get_permalink()); $post_title = urlencode(get_the_title()); $social_buttons = ' <div class="social-share-buttons"> <span class="share-label">分享: </span> <a href="https://www.facebook.com/sharer/sharer.php?u=' . $post_url . '" target="_blank" class="social-button facebook" aria-label="分享到Facebook"> <i class="fab fa-facebook-f"></i> </a> <a href="https://twitter.com/intent/tweet?url=' . $post_url . '&text=' . $post_title . '" target="_blank" class="social-button twitter" aria-label="分享到Twitter"> <i class="fab fa-twitter"></i> </a> <a href="https://www.linkedin.com/shareArticle?mini=true&url=' . $post_url . '&title=' . $post_title . '" target="_blank" class="social-button linkedin" aria-label="分享到LinkedIn"> <i class="fab fa-linkedin-in"></i> </a> <a href="https://api.whatsapp.com/send?text=' . $post_title . ' ' . $post_url . '" target="_blank" class="social-button whatsapp" aria-label="分享到WhatsApp"> <i class="fab fa-whatsapp"></i> </a> </div>'; $content .= $social_buttons; } return $content; } add_filter('the_content', 'add_social_share_buttons'); 4.2 添加阅读进度条 创建js/reading-progress.js文件: document.addEventListener('DOMContentLoaded', function() { // 创建进度条元素 const progressBar = document.createElement('div'); progressBar.className = 'reading-progress-bar'; progressBar.setAttribute('role', 'progressbar'); progressBar.setAttribute('aria-valuemin', '0'); progressBar.setAttribute('aria-valuemax', '100'); // 将进度条添加到页面顶部 document.body.prepend(progressBar); // 更新进度条函数 function updateReadingProgress() { const windowHeight = window.innerHeight; const documentHeight = document.documentElement.scrollHeight - windowHeight; const scrolled = window.scrollY; const progress = (scrolled / documentHeight) * 100; progressBar.style.width = progress + '%'; progressBar.setAttribute('aria-valuenow', Math.round(progress)); // 添加颜色变化效果 if (progress > 90) { progressBar.classList.add('complete'); } else { progressBar.classList.remove('complete'); } } // 监听滚动事件 window.addEventListener('scroll', updateReadingProgress); // 初始调用 updateReadingProgress();}); 在`style.css`中添加进度条样式: .reading-progress-bar { position: fixed; top: 0; left: 0; width: 0%; height: 4px; background-color: var(--primary-color); z-index: 9999; transition: width 0.1s ease;} .reading-progress-bar.complete { background-color: var(--secondary-color);} ### 4.3 添加回到顶部按钮 创建`js/back-to-top.js`文件: document.addEventListener('DOMContentLoaded', function() { // 创建按钮元素 const backToTopButton = document.createElement('button'); backToTopButton.id = 'back-to-top'; backToTopButton.className = 'back-to-top'; backToTopButton.setAttribute('aria-label', '回到顶部'); backToTopButton.innerHTML = '↑'; // 将按钮添加到页面 document.body.appendChild(backToTopButton); // 显示/隐藏按钮 function toggleBackToTopButton() { if (window.scrollY > 300) { backToTopButton.classList.add('visible'); } else { backToTopButton.classList.remove('visible'); } } // 回到顶部功能 function scrollToTop() { window.scrollTo({ top: 0, behavior: 'smooth' }); } // 监听滚动事件 window.addEventListener('scroll', toggleBackToTopButton); // 点击事件 backToTopButton.addEventListener('click', scrollToTop); // 初始调用 toggleBackToTopButton();}); 在`style.css`中添加样式: .back-to-top { position: fixed; bottom: 30px; right: 30px; width: 50px; height: 50px; background-color: var(--primary-color); color: white; border: none; border-radius: 50%; font-size: 20px; cursor: pointer; opacity: 0; visibility: hidden; transform: translateY(20px); transition: all 0.3s ease; z-index: 999; box-shadow: 0 2px 10px var(--shadow-color);} .back-to-top.visible { opacity: 1; visibility: visible; transform: translateY(0);} .back-to-top:hover { background-color: var(--secondary-color); transform: translateY(-5px);} ### 4.4 添加暗色模式下的工具样式调整 在`dark-mode.css`中添加工具适配样式: / 社交分享按钮在暗色模式下的适配 /[data-theme="dark"] .social-button { background-color: #333; color: #e0e0e0;} [data-theme="dark"] .social-button:hover { background-color: #444;} / 阅读进度条在暗色模式下的适配 /[data-theme="dark"] .reading-progress-bar { background-color: var(--primary-color);} [data-theme="dark"] .reading-progress-bar.complete { background-color: var(--secondary-color);} / 回到顶部按钮在暗色模式下的适配 /[data-theme="dark"] .back-to-top { background-color: var(--primary-color); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);} [data-theme="dark"] .back-to-top:hover { background-color: var(--secondary-color);} ## 第五部分:高级功能与优化 ### 5.1 添加主题预设方案 在`functions.php`中添加预设功能: // 添加主题预设方案function add_theme_presets($wp_customize) { // 添加预设部分 $wp_customize->add_section('theme_presets_section', array( 'title' => __('主题预设', 'my-custom-theme'), 'priority' => 10, )); // 预设选择设置 $wp_customize->add_setting('theme_preset_setting', array( 'default' => 'default', 'transport' => 'refresh', 'sanitize_callback' => 'sanitize_text_field', )); // 预设选择控件 $wp_customize->add_control('theme_preset_control', array( 'label' => __('选择预设方案', 'my-custom-theme'), 'section' => 'theme_presets_section', 'settings' => 'theme_preset_setting', 'type' => 'select', 'choices' => array( 'default' => '默认主题', 'ocean' => '海洋蓝', 'forest' => '森林绿', 'sunset' => '日落橙', 'midnight' => '午夜紫', ), )); // 应用预设的AJAX处理 add_action('wp_ajax_apply_theme_preset', 'apply_theme_preset_callback'); add_action('wp_ajax_nopriv_apply_theme_preset', 'apply_theme_preset_callback');}add_action('customize_register', 'add_theme_presets'); // 应用预设的回调函数function apply_theme_preset_callback() { $preset = sanitize_text_field($_POST['preset']); $presets = array( 'default' => array( 'primary_color' => '#3498db', 'background_color' => '#ffffff', 'text_color' => '#333333', ), 'ocean' => array( 'primary_color' => '#1abc9c', 'background_color' => '#ecf0f1', 'text_color' => '#2c3e50', ), 'forest' => array( 'primary_color' => '#27ae60', 'background_color' => '#f9f9f9', 'text_color' => '#2c3e50', ), 'sunset' => array( 'primary_color' => '#e74c3c', 'background_color' => '#fef9e7', 'text_color' => '#34495e', ), 'midnight' => array( 'primary_color' => '#9b59b6', 'background_color' => '#2c3e50', 'text_color' => '#ecf0f1', ), ); if (isset($presets[$preset])) { set_theme_mod('primary_color_setting', $presets[$preset]['primary_color']); set_theme_mod('background_color_setting', $presets[$preset]['background_color']); set_theme_mod('text_color_setting', $presets[$preset]['text_color']); wp_send_json_success(array( 'message' => '预设应用成功', 'colors' => $presets[$preset] )); } else { wp_send_json_error('无效的预设方案'); }} ### 5.2 添加导出/导入主题设置功能 在`functions.php`中添加: // 添加导出/导入功能function add_export_import_features() { // 导出设置 if (isset($_GET['export_theme_settings']) && current_user_can('edit_theme_options')) { $settings = array( 'primary_color' => get_theme_mod('primary_color_setting'), 'background_color' => get_theme_mod('background_color_setting'), 'text_color' => get_theme_mod('text_color_setting'), 'font_family' => get_theme_mod('font_family_setting'), 'base_font_size' => get_theme_mod('base_font_size_setting'), ); header('Content-Type: application/json'); header('Content-Disposition: attachment; filename="theme-settings-' . date('Y-m-d') . '.json"'); echo json_encode($settings, JSON_PRETTY_PRINT); exit; } // 导入设置页面 add_submenu_page( 'themes.php', '导入主题设置', '导入设置', 'edit_theme_options', 'import-theme-settings', 'import_theme_settings_page' );}add_action('admin_init', 'add_export_import_features'); // 导入设置页面function import_theme_settings_page() { ?> <div class="wrap"> <h1>导入主题设置</h1> <?php if (isset($_POST['import_settings']) && isset($_FILES['settings_file'])) { $file_content = file_get_contents($_FILES['settings_file']['tmp_name']); $settings = json_decode($file_content, true); if ($settings) { foreach ($settings as $key => $value) { switch ($key) { case 'primary_color': set_theme_mod('primary_color_setting', $value); break; case 'background_color': set_theme_mod('background_color_setting', $value); break; case 'text_color': set_theme_mod('text_color_setting', $value); break; case 'font_family': set_theme_mod('font_family_setting', $value); break; case 'base_font_size': set_theme_mod('base_font_size_setting', $value); break; } } echo '<div class="notice notice-success"><p>设置导入成功!</p></div>'; } else { echo '<div class="notice notice-error"><p>导入失败,请检查文件格式。</p></div>'; } } ?> <form method="post" enctype="multipart/form-data"> <p> <label for="settings_file">选择设置文件 (JSON格式):</label><br> <input type="file" name="settings_file" id="settings_file" accept=".json"> </p> <p> <input type="submit" name="import_settings" class="button button-primary" value="导入设置"> </p> </form> <hr> <h2>导出当前设置</h2> <p> <a href="<?php echo admin_url('themes.php?export_theme_settings=1'); ?>" class="button button-secondary"> 导出设置为JSON文件 </a> </p> </div> <?php} ### 5.3 添加性能优化 在`functions.php`中添加性能优化代码: // 优化CSS加载function optimize_css_loading() { // 内联关键CSS $critical_css = ' :root { --primary-color: ' . get_theme_mod('primary_color_setting', '#3498db') . '; --background-color: ' . get_theme_mod('background_color_setting', '#ffffff') . '; --text-color: ' . get_theme_mod('text_color_setting', '#333333') . '; } body { font-family: ' . get_theme_mod('font_family_setting', 'Arial, sans-serif') . '; background-color: var(--background-color); color: var(--text-color); } .dark-mode-toggle { position: fixed; top: 20px; right: 20px; z-index: 1000; } '; echo '<style id="critical-css">' . $critical_css . '</style>'; // 延迟加载非关键CSS add_filter('style_loader_tag', 'defer_non_critical_css', 10, 2);}add_action('wp_head', 'optimize_css_loading', 1); function defer_non_critical_css($html, $handle) { if (strpos($handle, 'dark-mode') !== false || strpos($handle, 'dynamic-theme') !== false) { return str_replace("media='all'", "media='print' onload="this.media='all'"", $html); } return $html;} // 添加资源提示function add_resource_hints() { echo '<link rel="preconnect" href="https://fonts.googleapis.com">'; echo '<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>';}add_action('wp_head', 'add_resource_hints', 2); ## 第六部分:测试与部署 ### 6.1 功能测试清单 在部署前,请测试以下功能: 1. **深色模式切换** - 按钮点击切换是否正常 - 本地存储是否保存用户选择 - 系统主题偏好检测是否工作 - 切换动画是否流畅 2. **主题定制器** - 所有颜色选择器是否工作 - 实时预览是否正常 - 字体和字号设置是否生效 - 预设方案是否正常应用 3. **小工具功能** - 社交分享按钮是否显示 - 阅读进度条是否准确 - 回到顶部按钮是否正常 - 所有功能在深色模式下是否适配 4. **性能测试** - 页面加载速度 - 移动设备兼容性 - 不同浏览器兼容性 ### 6.2 部署注意事项 1. **备份原始文件**:部署前备份所有修改的文件 2. **分阶段部署**:先在测试环境验证,再部署到生产环境 3. **用户通知**:如果是对现有网站的更新,通知用户新功能 4. **收集反馈**:部署后收集用户反馈,持续改进 ### 6.3 维护与更新 1. **定期检查兼容性**:随着WordPress更新,检查功能兼容性 2. **性能监控**:监控网站性能,确保新功能不影响速度 3. **用户反馈循环**:建立用户反馈机制,持续改进功能 4. **安全更新**:定期更新安全补丁,确保代码安全 ## 结论 通过本文的详细步骤,你已经学会了如何在WordPress中通过代码二次开发实现深色模式切换和个性化主题定制器,并集成了多种常用互联网小工具。这些功能不仅提升了网站的用户体验,还展示了WordPress强大的自定义能力。 关键要点总结: 1. **深色模式**通过CSS变量和JavaScript实现,兼顾系统偏好和用户选择 2. **主题定制器**利用WordPress原生API,提供直观的定制界面 3. **小工具集成**增强了网站功能性和用户参与度 4. **性能优化**确保了功能的流畅运行 随着用户对个性化体验需求的不断增长,这些功能将成为现代WordPress网站的标配。通过不断测试和优化,你可以进一步扩展这些功能,创造更加独特和用户友好的网站体验。
发表评论实战教程:为WordPress网站集成智能化的内容原创性检测与版权保护系统 引言:数字时代的内容保护挑战 在当今信息爆炸的互联网时代,内容创作已成为网站运营的核心。然而,随着内容创作的普及,抄袭、盗用和未经授权的转载问题也日益严重。对于WordPress网站管理员和内容创作者而言,保护原创内容不仅是维护品牌声誉的需要,更是保障内容投资回报的关键。 传统的版权保护方法往往被动且效率低下,而智能化的内容原创性检测与版权保护系统则能主动识别侵权行为,为原创内容提供全方位保护。本教程将深入探讨如何通过WordPress代码二次开发,集成一套完整的智能化内容保护系统,同时实现多种实用的互联网小工具功能。 第一部分:系统架构设计与技术选型 1.1 系统核心功能规划 一个完整的智能化内容保护系统应包含以下核心功能: 原创性检测:自动检测新发布内容与互联网现有内容的相似度 版权水印:为多媒体内容添加隐形或可见的版权标识 侵权监控:定期扫描网络,发现未经授权的内容使用 自动化维权:发送侵权通知、生成侵权报告 访问控制:防止内容被非法复制、下载或截图 1.2 技术栈选择 为实现上述功能,我们需要选择合适的技术方案: 核心平台:WordPress 5.0+(支持REST API和现代开发特性) 检测引擎:结合本地算法与第三方API(如Copyscape、Grammarly API) 水印技术:使用PHP GD库或ImageMagick进行图像处理 监控机制:基于WordPress Cron的定时任务系统 前端保护:JavaScript内容保护技术 数据存储:MySQL数据库配合WordPress自定义表 1.3 系统架构图 用户发布内容 → WordPress → 原创性检测模块 → 内容处理模块 ↓ ↓ ↓ 版权水印添加 ← 检测结果分析 ← 与外部API交互 ↓ ↓ 数据库存储 侵权内容记录 ↓ ↓ 前端展示 侵权监控警报 第二部分:WordPress开发环境配置 2.1 开发环境搭建 首先,我们需要配置一个适合WordPress插件开发的环境: // 创建插件主文件:wp-content/plugins/content-protection-system/content-protection-system.php <?php /** * Plugin Name: 智能内容保护系统 * Plugin URI: https://yourwebsite.com/ * Description: 为WordPress网站提供智能化内容原创性检测与版权保护功能 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later * Text Domain: content-protection-system */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('CPS_VERSION', '1.0.0'); define('CPS_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('CPS_PLUGIN_URL', plugin_dir_url(__FILE__)); define('CPS_PLUGIN_BASENAME', plugin_basename(__FILE__)); 2.2 数据库表设计 我们需要创建自定义数据库表来存储检测结果和侵权记录: // 在插件激活时创建数据库表 register_activation_hook(__FILE__, 'cps_create_database_tables'); function cps_create_database_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name_content_checks = $wpdb->prefix . 'cps_content_checks'; $table_name_infringements = $wpdb->prefix . 'cps_infringements'; // 内容检测记录表 $sql_content_checks = "CREATE TABLE IF NOT EXISTS $table_name_content_checks ( id bigint(20) NOT NULL AUTO_INCREMENT, post_id bigint(20) NOT NULL, check_date datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, originality_score float NOT NULL, sources_found text, check_method varchar(50) NOT NULL, check_status varchar(20) DEFAULT 'pending', PRIMARY KEY (id), KEY post_id (post_id), KEY check_date (check_date) ) $charset_collate;"; // 侵权记录表 $sql_infringements = "CREATE TABLE IF NOT EXISTS $table_name_infringements ( id bigint(20) NOT NULL AUTO_INCREMENT, original_post_id bigint(20) NOT NULL, infringing_url varchar(500) NOT NULL, detection_date datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, similarity_percent float NOT NULL, action_taken varchar(50), status varchar(20) DEFAULT 'pending', notes text, PRIMARY KEY (id), KEY original_post_id (original_post_id), KEY infringing_url (infringing_url(191)) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql_content_checks); dbDelta($sql_infringements); } 第三部分:原创性检测模块实现 3.1 本地文本相似度算法 首先实现一个本地的文本相似度检测算法作为基础检测层: class ContentOriginalityChecker { /** * 计算两段文本的相似度 */ public function calculate_similarity($text1, $text2) { // 文本预处理 $text1 = $this->preprocess_text($text1); $text2 = $this->preprocess_text($text2); // 使用余弦相似度算法 $vector1 = $this->text_to_vector($text1); $vector2 = $this->text_to_vector($text2); return $this->cosine_similarity($vector1, $vector2); } /** * 文本预处理 */ private function preprocess_text($text) { // 转换为小写 $text = strtolower($text); // 移除HTML标签 $text = strip_tags($text); // 移除标点符号和特殊字符 $text = preg_replace('/[^p{L}p{N}s]/u', ' ', $text); // 移除多余空格 $text = preg_replace('/s+/', ' ', $text); return trim($text); } /** * 将文本转换为词频向量 */ private function text_to_vector($text) { $words = explode(' ', $text); $vector = array(); foreach ($words as $word) { // 移除停用词(简单示例,实际应使用更完整的停用词列表) $stopwords = array('的', '了', '在', '是', '我', '有', '和', '就', '不', '人', '都', '一', '一个', '上', '也', '很', '到', '说', '要', '去', '你', '会', '着', '没有', '看', '好', '自己', '这'); if (in_array($word, $stopwords) || strlen($word) < 2) { continue; } if (!isset($vector[$word])) { $vector[$word] = 0; } $vector[$word]++; } return $vector; } /** * 计算余弦相似度 */ private function cosine_similarity($vector1, $vector2) { // 获取所有唯一词 $all_words = array_unique(array_merge(array_keys($vector1), array_keys($vector2))); // 计算点积和模长 $dot_product = 0; $magnitude1 = 0; $magnitude2 = 0; foreach ($all_words as $word) { $value1 = isset($vector1[$word]) ? $vector1[$word] : 0; $value2 = isset($vector2[$word]) ? $vector2[$word] : 0; $dot_product += $value1 * $value2; $magnitude1 += $value1 * $value1; $magnitude2 += $value2 * $value2; } $magnitude1 = sqrt($magnitude1); $magnitude2 = sqrt($magnitude2); if ($magnitude1 == 0 || $magnitude2 == 0) { return 0; } return $dot_product / ($magnitude1 * $magnitude2); } /** * 检测内容原创性 */ public function check_originality($content, $title = '') { $results = array( 'score' => 100, // 默认100%原创 'sources' => array(), 'method' => 'local' ); // 从内容中提取关键片段进行搜索 $search_queries = $this->extract_search_queries($content, $title); foreach ($search_queries as $query) { $search_results = $this->search_online($query); foreach ($search_results as $result) { $similarity = $this->calculate_similarity($content, $result['content']); if ($similarity > 0.3) { // 相似度超过30%视为可能侵权 $results['sources'][] = array( 'url' => $result['url'], 'title' => $result['title'], 'similarity' => round($similarity * 100, 2) ); // 更新最低原创度分数 $results['score'] = min($results['score'], 100 - round($similarity * 100, 2)); } } } return $results; } /** * 从内容中提取搜索查询词 */ private function extract_search_queries($content, $title) { $queries = array(); // 使用标题作为搜索词 if (!empty($title)) { $queries[] = $title; } // 提取内容中的关键句子 $sentences = preg_split('/[。.!?]/u', $content); foreach ($sentences as $sentence) { $sentence = trim($sentence); if (mb_strlen($sentence) > 15 && mb_strlen($sentence) < 100) { $queries[] = $sentence; } // 限制查询数量 if (count($queries) >= 5) { break; } } return $queries; } /** * 模拟在线搜索(实际应调用搜索引擎API) */ private function search_online($query) { // 这里应该调用搜索引擎API,如Google Custom Search API // 以下为模拟数据 return array(); } } 3.2 集成第三方原创性检测API 为了获得更准确的检测结果,我们可以集成第三方API: class APIBasedOriginalityChecker { private $api_keys = array(); public function __construct() { // 从WordPress选项获取API密钥 $this->api_keys = get_option('cps_api_keys', array()); } /** * 使用Copyscape API检测内容原创性 */ public function check_with_copyscape($content, $title) { $api_key = isset($this->api_keys['copyscape']) ? $this->api_keys['copyscape'] : ''; if (empty($api_key)) { return array('error' => 'Copyscape API密钥未配置'); } // 准备API请求 $url = 'http://www.copyscape.com/api/'; $args = array( 'method' => 'POST', 'timeout' => 30, 'body' => array( 'k' => $api_key, 'o' => 'csearch', 'e' => 'UTF-8', 't' => $title, 'c' => $content, 'f' => 'xml' ) ); // 发送请求 $response = wp_remote_post($url, $args); if (is_wp_error($response)) { return array('error' => $response->get_error_message()); } $body = wp_remote_retrieve_body($response); // 解析XML响应 $xml = simplexml_load_string($body); $results = array( 'score' => 100, 'sources' => array(), 'method' => 'copyscape' ); if ($xml && isset($xml->result)) { foreach ($xml->result as $result) { $similarity = floatval($result->percent); $results['sources'][] = array( 'url' => (string)$result->url, 'title' => (string)$result->title, 'similarity' => $similarity ); $results['score'] = min($results['score'], 100 - $similarity); } } return $results; } /** * 使用Grammarly API检测内容原创性 */ public function check_with_grammarly($content) { $api_key = isset($this->api_keys['grammarly']) ? $this->api_keys['grammarly'] : ''; if (empty($api_key)) { return array('error' => 'Grammarly API密钥未配置'); } // 注意:Grammarly API需要商业授权,此处为示例代码结构 $url = 'https://api.grammarly.com/plagiarism-checker/v1/check'; $args = array( 'method' => 'POST', 'timeout' => 30, 'headers' => array( 'Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json' ), 'body' => json_encode(array( 'text' => $content, 'language' => 'en' )) ); $response = wp_remote_post($url, $args); // 处理响应... return array('score' => 100, 'sources' => array(), 'method' => 'grammarly'); } } 第四部分:版权保护与水印系统 4.1 图像水印功能 为上传的图像自动添加版权水印: class ImageWatermark { /** * 为上传的图像添加水印 */ public function add_watermark_to_image($image_path, $watermark_type = 'text') { // 获取图像信息 $image_info = getimagesize($image_path); $mime_type = $image_info['mime']; // 根据MIME类型创建图像资源 switch ($mime_type) { case 'image/jpeg': $image = imagecreatefromjpeg($image_path); break; case 'image/png': $image = imagecreatefrompng($image_path); break; case 'image/gif': $image = imagecreatefromgif($image_path); break; default: return false; } if (!$image) { return false; } // 获取图像尺寸 $image_width = imagesx($image); $image_height = imagesy($image); if ($watermark_type == 'text') { $this->add_text_watermark($image, $image_width, $image_height); } else { $this->add_image_watermark($image, $image_width, $image_height); } // 保存图像 switch ($mime_type) { case 'image/jpeg': imagejpeg($image, $image_path, 90); break; case 'image/png': imagepng($image, $image_path, 9); break; case 'image/gif': imagegif($image, $image_path); break; } // 释放内存 imagedestroy($image); return true; } /** * 添加文字水印 */ private function add_text_watermark($image, $image_width, $image_height) { // 水印文字 $site_url = get_site_url(); $watermark_text = "© " . date('Y') . " " . get_bloginfo('name') . " - " . $site_url; // 水印字体大小(根据图像尺寸动态调整) $font_size = max(12, $image_width / 50); // 加载字体 $font_path = CPS_PLUGIN_DIR . 'assets/fonts/arial.ttf'; // 计算文字尺寸 $text_box = imagettfbbox($font_size, 0, $font_path, $watermark_text); $text_width = $text_box[2] - $text_box[0]; $text_height = $text_box[1] - $text_box[7]; // 水印位置(右下角,留边距) $margin = 20; $x = $image_width - $text_width - $margin; $y = $image_height - $margin; // 水印颜色(半透明白色) $color = imagecolorallocatealpha($image, 255, 255, 255, 60); // 添加文字水印 imagettftext($image, $font_size, 0, $x, $y, $color, $font_path, $watermark_text); // 添加第二层更透明的水印(防移除) $color2 = imagecolorallocatealpha($image, 255, 255, 255, 10); $pattern_text = get_bloginfo('name') . " " . $site_url; 4.2 数字水印与隐形标识 除了可见水印,我们还可以添加隐形数字水印,用于追踪图像来源: /** * 添加隐形数字水印(LSB隐写术) */ private function add_digital_watermark($image_path) { // 读取图像为二进制数据 $image_data = file_get_contents($image_path); // 生成水印信息 $site_url = get_site_url(); $post_id = get_the_ID(); $watermark_data = "COPYRIGHT:" . $site_url . ":POST:" . $post_id . ":DATE:" . date('Y-m-d'); // 将水印信息转换为二进制 $binary_watermark = ''; for ($i = 0; $i < strlen($watermark_data); $i++) { $binary_watermark .= sprintf('%08b', ord($watermark_data[$i])); } // 添加结束标记 $binary_watermark .= '00000000'; // 使用LSB隐写术嵌入水印 $watermark_length = strlen($binary_watermark); $image_length = strlen($image_data); // 确保图像足够大以容纳水印 if ($image_length < $watermark_length * 8) { return false; } // 在图像数据中嵌入水印 $watermark_index = 0; for ($i = 0; $i < $image_length && $watermark_index < $watermark_length; $i++) { // 跳过文件头(前100字节) if ($i < 100) continue; // 修改每个字节的最低位 $byte = ord($image_data[$i]); $bit = $binary_watermark[$watermark_index]; // 设置最低位 if ($bit == '1') { $byte = $byte | 1; // 设置最低位为1 } else { $byte = $byte & ~1; // 设置最低位为0 } $image_data[$i] = chr($byte); $watermark_index++; } // 保存修改后的图像 file_put_contents($image_path, $image_data); return true; } /** * 检测并提取数字水印 */ public function detect_digital_watermark($image_path) { $image_data = file_get_contents($image_path); $image_length = strlen($image_data); $binary_data = ''; // 提取LSB位 $max_bits = 1000; // 限制提取的位数 $bits_extracted = 0; for ($i = 100; $i < $image_length && $bits_extracted < $max_bits; $i++) { $byte = ord($image_data[$i]); $lsb = $byte & 1; // 获取最低位 $binary_data .= $lsb; $bits_extracted++; } // 将二进制数据转换为字符串 $watermark_string = ''; for ($j = 0; $j < strlen($binary_data); $j += 8) { $byte_binary = substr($binary_data, $j, 8); // 检查结束标记 if ($byte_binary == '00000000') { break; } if (strlen($byte_binary) == 8) { $char = chr(bindec($byte_binary)); // 只接受可打印字符 if (ctype_print($char) || $char == ':') { $watermark_string .= $char; } else { // 遇到非打印字符,可能不是有效水印 break; } } } // 验证水印格式 if (strpos($watermark_string, 'COPYRIGHT:') === 0) { $parts = explode(':', $watermark_string); if (count($parts) >= 5) { return array( 'type' => 'copyright', 'site' => $parts[1], 'post_id' => $parts[3], 'date' => $parts[5] ); } } return false; } 4.3 前端内容保护 防止用户轻易复制网站内容: class FrontendContentProtection { public function __construct() { // 在文章内容加载时添加保护 add_filter('the_content', array($this, 'protect_content')); // 添加前端脚本和样式 add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts')); } /** * 保护文章内容 */ public function protect_content($content) { if (!is_single() && !is_page()) { return $content; } // 只在需要保护的文章类型上应用 $protected_types = get_option('cps_protected_post_types', array('post', 'page')); $current_type = get_post_type(); if (!in_array($current_type, $protected_types)) { return $content; } // 添加保护层 $protected_content = '<div class="cps-protected-content" data-post-id="' . get_the_ID() . '">'; $protected_content .= '<div class="cps-content-wrapper">' . $content . '</div>'; $protected_content .= $this->get_protection_overlay(); $protected_content .= '</div>'; return $protected_content; } /** * 获取保护覆盖层HTML */ private function get_protection_overlay() { $site_name = get_bloginfo('name'); $current_year = date('Y'); return ' <div class="cps-protection-overlay" style=" position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: transparent; z-index: 9998; pointer-events: none; user-select: none; "></div> <div class="cps-copyright-notice" style=" position: fixed; bottom: 20px; right: 20px; background: rgba(0,0,0,0.8); color: white; padding: 10px 15px; border-radius: 5px; font-size: 12px; z-index: 10000; display: none; "> 本文受版权保护 © ' . $current_year . ' ' . $site_name . ' - 未经授权禁止复制 </div>'; } /** * 添加前端脚本 */ public function enqueue_scripts() { if (!is_single() && !is_page()) { return; } wp_enqueue_script( 'cps-frontend-protection', CPS_PLUGIN_URL . 'assets/js/frontend-protection.js', array('jquery'), CPS_VERSION, true ); wp_enqueue_style( 'cps-frontend-style', CPS_PLUGIN_URL . 'assets/css/frontend-style.css', array(), CPS_VERSION ); // 传递数据到JavaScript wp_localize_script('cps-frontend-protection', 'cps_data', array( 'ajax_url' => admin_url('admin-ajax.php'), 'protection_enabled' => true, 'copy_warning' => '本文受版权保护,如需引用请注明出处。', 'print_warning' => '打印功能已禁用,本文受版权保护。' )); } } 创建前端JavaScript保护脚本: // assets/js/frontend-protection.js jQuery(document).ready(function($) { // 禁止右键菜单 $('.cps-protected-content').on('contextmenu', function(e) { showCopyrightNotice(); return false; }); // 禁止文本选择 $('.cps-protected-content').css({ '-webkit-user-select': 'none', '-moz-user-select': 'none', '-ms-user-select': 'none', 'user-select': 'none' }); // 禁止拖拽 $('.cps-protected-content').on('dragstart', function(e) { return false; }); // 检测复制操作 document.addEventListener('copy', function(e) { if ($(e.target).closest('.cps-protected-content').length) { e.preventDefault(); // 获取文章基本信息 var postTitle = $('h1.entry-title').text() || document.title; var siteName = document.title.split('|')[0] || window.location.hostname; var currentUrl = window.location.href; // 创建带有引用的文本 var selectedText = window.getSelection().toString(); var creditedText = selectedText + "nn—— 摘自《" + postTitle + "》,来源:" + siteName + " (" + currentUrl + ")"; // 将带引用的文本放入剪贴板 e.clipboardData.setData('text/plain', creditedText); showCopyrightNotice(); // 记录复制行为 $.ajax({ url: cps_data.ajax_url, method: 'POST', data: { action: 'cps_log_copy_action', post_id: $('.cps-protected-content').data('post-id'), text_length: selectedText.length } }); } }); // 检测打印操作 window.addEventListener('beforeprint', function(e) { if ($('.cps-protected-content').length) { alert(cps_data.print_warning); e.preventDefault(); return false; } }); // 检测开发者工具 var devtools = /./; devtools.toString = function() { showCopyrightNotice(); return 'copyright_protection_enabled'; }; console.log('%c', devtools); // 显示版权通知 function showCopyrightNotice() { $('.cps-copyright-notice').fadeIn().delay(3000).fadeOut(); } // 防止截图(通过CSS) document.addEventListener('keydown', function(e) { // 检测Print Screen键 if (e.keyCode === 44) { // Print Screen键 showCopyrightNotice(); // 临时添加防截图效果 $('body').addClass('cps-anti-screenshot'); setTimeout(function() { $('body').removeClass('cps-anti-screenshot'); }, 1000); } // 检测Ctrl+P(打印) if ((e.ctrlKey || e.metaKey) && e.keyCode === 80) { e.preventDefault(); showCopyrightNotice(); return false; } }); }); 第五部分:侵权监控与自动化维权 5.1 定期网络监控 class InfringementMonitor { private $search_engines = array( 'google' => 'https://www.google.com/search?q=', 'bing' => 'https://www.bing.com/search?q=' ); /** * 初始化监控任务 */ public function init_monitoring() { // 每天执行一次侵权监控 if (!wp_next_scheduled('cps_daily_infringement_check')) { wp_schedule_event(time(), 'daily', 'cps_daily_infringement_check'); } add_action('cps_daily_infringement_check', array($this, 'run_daily_check')); } /** * 执行每日侵权检查 */ public function run_daily_check() { // 获取最近30天发布的文章 $recent_posts = get_posts(array( 'post_type' => 'post', 'post_status' => 'publish', 'date_query' => array( array( 'after' => '30 days ago' ) ), 'posts_per_page' => 20 )); foreach ($recent_posts as $post) { $this->check_post_infringements($post); } } /** * 检查单篇文章的侵权情况 */ public function check_post_infringements($post) { // 提取文章特征 $signatures = $this->extract_content_signatures($post->post_content); foreach ($signatures as $signature) { $search_results = $this->search_content_online($signature); foreach ($search_results as $result) { // 检查是否来自自己的网站 if ($this->is_own_domain($result['url'])) { continue; } // 获取疑似侵权页面内容 $infringing_content = $this->fetch_page_content($result['url']); if ($infringing_content) { $similarity = $this->calculate_content_similarity( $post->post_content, $infringing_content ); // 如果相似度超过阈值,记录侵权 if ($similarity > 0.5) { // 50%相似度 $this->record_infringement( $post->ID, $result['url'], $similarity, $infringing_content ); } } } } } /** * 提取内容特征(用于搜索) */ private function extract_content_signatures($content) { $signatures = array(); // 清理内容 $clean_content = strip_tags($content); $clean_content = preg_replace('/s+/', ' ', $clean_content); // 提取独特句子 $sentences = preg_split('/[。.!?]/u', $clean_content); foreach ($sentences as $sentence) { $sentence = trim($sentence); // 选择长度适中且包含关键词的句子 if (mb_strlen($sentence) > 20 && mb_strlen($sentence) < 100) { // 检查句子是否包含重要关键词 if ($this->contains_important_keywords($sentence)) { $signatures[] = $sentence; } } // 限制特征数量 if (count($signatures) >= 5) { break; } } return $signatures; } /** * 检查句子是否包含重要关键词 */ private function contains_important_keywords($sentence) { // 这里可以定义重要关键词列表 $important_keywords = get_option('cps_important_keywords', array()); if (empty($important_keywords)) { // 如果没有设置,使用默认判断逻辑 $words = explode(' ', $sentence); return count($words) > 5; } foreach ($important_keywords as $keyword) { if (stripos($sentence, $keyword) !== false) { return true; } } return false; } /** * 在线搜索内容 */ private function search_content_online($query) { $results = array(); // 使用多个搜索引擎进行搜索 foreach ($this->search_engines as $engine => $url) { $search_url = $url . urlencode('"' . $query . '"'); // 使用WordPress HTTP API获取搜索结果 $response = wp_remote_get($search_url, array( 'timeout' => 30, 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' )); if (!is_wp_error($response)) { $body = wp_remote_retrieve_body($response); $engine_results = $this->parse_search_results($body, $engine); $results = array_merge($results, $engine_results); } } return $results; } /** * 解析搜索结果 */ private function parse_search_results($html, $engine) { $results = array(); if ($engine == 'google') { // 解析Google搜索结果 preg_match_all('/<a href="/url?q=([^&]+)[^>]*>([^<]+)</a>/', $html, $matches); if (!empty($matches[1])) { for ($i = 0; $i < count($matches[1]); $i++) { $url = urldecode($matches[1][$i]); $title = strip_tags($matches[2][$i]); // 跳过Google自己的链接 if (strpos($url, 'google.com') === false) { $results[] = array( 'url' => $url, 'title' => $title, 'engine' => 'google' ); } } } } return $results; } /** * 记录侵权信息 */ private function record_infringement($post_id, $infringing_url, $similarity, $content) { global $wpdb; $table_name = $wpdb->prefix . 'cps_infringements'; // 检查是否已记录 $existing = $wpdb->get_var($wpdb->prepare( "SELECT id FROM $table_name WHERE original_post_id = %d AND infringing_url = %s", $post_id, $infringing_url )); if ($existing) { // 更新现有记录 $wpdb->update( $table_name, array( 'similarity_percent' => $similarity * 100, 'detection_date' => current_time('mysql'), 'status' => 'detected' ), array('id' => $existing) ); } else { // 插入新记录 $wpdb->insert( $table_name, array( 'original_post_id' => $post_id, 'infringing_url' => $infringing_url,
发表评论详细指南:在WordPress中开发集成在线简历制作与智能职位匹配工具 摘要 随着就业市场竞争日益激烈,求职者和招聘方都需要更高效的工具来连接彼此。本文将详细介绍如何在WordPress平台上开发一个集在线简历制作与智能职位匹配功能于一体的工具。通过WordPress代码二次开发,我们将实现一个功能完整、用户体验优秀的职业服务平台,帮助求职者创建专业简历,同时利用智能算法匹配最适合的职位机会。 一、项目概述与规划 1.1 项目背景与市场需求 在数字化招聘时代,传统的简历投递方式已无法满足现代求职市场的需求。根据最新统计,超过70%的求职者希望通过在线平台创建和管理简历,而招聘方则期望更精准的候选人匹配机制。WordPress作为全球最流行的内容管理系统,拥有强大的扩展性和庞大的开发者社区,是构建此类工具的绝佳平台。 本项目旨在开发一个集成以下核心功能的WordPress插件: 可视化在线简历编辑器 智能职位匹配引擎 用户管理系统 数据分析与报告功能 1.2 技术架构设计 我们将采用分层架构设计: 表现层:使用WordPress主题模板和前端框架(如Vue.js或React)构建用户界面 业务逻辑层:通过自定义插件处理核心业务逻辑 数据层:扩展WordPress数据库结构,存储简历、职位和匹配数据 服务层:集成第三方API(如职位数据源、AI分析服务) 1.3 开发环境准备 在开始开发前,需要配置以下环境: 本地开发环境:XAMPP/MAMP或Local by Flywheel WordPress安装(建议最新版本) 代码编辑器:VS Code或PHPStorm 版本控制:Git 调试工具:Query Monitor、Debug Bar 二、数据库设计与扩展 2.1 自定义数据表设计 WordPress默认的数据表无法满足复杂应用需求,我们需要创建以下自定义表: -- 简历表 CREATE TABLE wp_resume_builder_resumes ( resume_id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT(20) UNSIGNED NOT NULL, resume_title VARCHAR(255) NOT NULL, resume_data LONGTEXT NOT NULL, -- JSON格式存储简历内容 created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, is_public TINYINT(1) DEFAULT 1, PRIMARY KEY (resume_id), KEY user_id (user_id) ); -- 职位表 CREATE TABLE wp_resume_builder_jobs ( job_id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, company_id BIGINT(20) UNSIGNED NOT NULL, job_title VARCHAR(255) NOT NULL, job_description LONGTEXT NOT NULL, requirements LONGTEXT NOT NULL, location VARCHAR(255), job_type VARCHAR(50), -- 全职、兼职等 salary_range VARCHAR(100), posted_date DATETIME NOT NULL, expiry_date DATETIME, is_active TINYINT(1) DEFAULT 1, PRIMARY KEY (job_id) ); -- 匹配记录表 CREATE TABLE wp_resume_builder_matches ( match_id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, resume_id BIGINT(20) UNSIGNED NOT NULL, job_id BIGINT(20) UNSIGNED NOT NULL, match_score DECIMAL(5,2) NOT NULL, -- 匹配分数0-100 match_reasons TEXT, -- 匹配原因分析 created_at DATETIME NOT NULL, PRIMARY KEY (match_id), KEY resume_job (resume_id, job_id) ); 2.2 数据库操作类实现 创建数据库操作类来管理自定义表: <?php class Resume_Builder_DB { private static $instance = null; private $wpdb; private $resumes_table; private $jobs_table; private $matches_table; private function __construct() { global $wpdb; $this->wpdb = $wpdb; $this->resumes_table = $wpdb->prefix . 'resume_builder_resumes'; $this->jobs_table = $wpdb->prefix . 'resume_builder_jobs'; $this->matches_table = $wpdb->prefix . 'resume_builder_matches'; } public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } // 创建或更新简历 public function save_resume($user_id, $resume_data, $resume_id = null) { $data = array( 'user_id' => $user_id, 'resume_title' => sanitize_text_field($resume_data['title']), 'resume_data' => json_encode($resume_data), 'updated_at' => current_time('mysql') ); if ($resume_id) { // 更新现有简历 $where = array('resume_id' => $resume_id); return $this->wpdb->update($this->resumes_table, $data, $where); } else { // 创建新简历 $data['created_at'] = current_time('mysql'); return $this->wpdb->insert($this->resumes_table, $data); } } // 获取用户的所有简历 public function get_user_resumes($user_id) { $query = $this->wpdb->prepare( "SELECT * FROM {$this->resumes_table} WHERE user_id = %d ORDER BY updated_at DESC", $user_id ); return $this->wpdb->get_results($query); } // 更多数据库操作方法... } ?> 三、在线简历编辑器开发 3.1 前端编辑器界面 使用现代JavaScript框架构建响应式简历编辑器: <!-- 简历编辑器主界面结构 --> <div id="resume-builder-app"> <div class="resume-editor-container"> <!-- 左侧编辑面板 --> <div class="editor-panel"> <div class="section-tabs"> <button class="tab active" data-section="personal">个人信息</button> <button class="tab" data-section="experience">工作经历</button> <button class="tab" data-section="education">教育背景</button> <button class="tab" data-section="skills">技能专长</button> <button class="tab" data-section="projects">项目经验</button> </div> <div class="section-content"> <!-- 个人信息部分 --> <div class="section active" id="personal-section"> <div class="form-group"> <label>姓名</label> <input type="text" v-model="resume.personal.name" class="form-control"> </div> <div class="form-group"> <label>职业头衔</label> <input type="text" v-model="resume.personal.title" class="form-control"> </div> <!-- 更多字段... --> </div> <!-- 工作经历部分 --> <div class="section" id="experience-section"> <div class="experience-item" v-for="(exp, index) in resume.experience"> <h4>工作经历 {{ index + 1 }}</h4> <div class="form-group"> <label>公司名称</label> <input type="text" v-model="exp.company" class="form-control"> </div> <div class="form-group"> <label>职位</label> <input type="text" v-model="exp.position" class="form-control"> </div> <!-- 更多字段... --> </div> <button @click="addExperience">添加工作经历</button> </div> <!-- 其他部分... --> </div> </div> <!-- 右侧实时预览 --> <div class="preview-panel"> <div class="resume-preview"> <div class="resume-header"> <h1>{{ resume.personal.name }}</h1> <h2>{{ resume.personal.title }}</h2> </div> <!-- 实时预览内容... --> </div> </div> </div> <!-- 操作按钮 --> <div class="editor-actions"> <button class="btn btn-save" @click="saveResume">保存简历</button> <button class="btn btn-export" @click="exportPDF">导出PDF</button> <button class="btn btn-match" @click="findMatches">智能匹配职位</button> </div> </div> 3.2 简历数据模型与存储 定义简历数据结构并实现保存功能: // 简历数据模型 const resumeModel = { personal: { name: '', title: '', email: '', phone: '', location: '', summary: '' }, experience: [ { company: '', position: '', startDate: '', endDate: '', description: '', achievements: [] } ], education: [], skills: { technical: [], soft: [], languages: [] }, projects: [], settings: { template: 'modern', colorScheme: 'blue', fontSize: 'medium' } }; // Vue.js应用实例 const app = new Vue({ el: '#resume-builder-app', data: { resume: JSON.parse(JSON.stringify(resumeModel)), currentSection: 'personal', isLoading: false }, methods: { // 保存简历到服务器 async saveResume() { this.isLoading = true; try { const response = await fetch('/wp-json/resume-builder/v1/save', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': resumeBuilderData.nonce }, body: JSON.stringify({ resume: this.resume, title: this.resume.personal.name + '的简历' }) }); const result = await response.json(); if (result.success) { alert('简历保存成功!'); } else { alert('保存失败:' + result.message); } } catch (error) { console.error('保存错误:', error); alert('保存过程中发生错误'); } finally { this.isLoading = false; } }, // 添加工作经历 addExperience() { this.resume.experience.push({ company: '', position: '', startDate: '', endDate: '', description: '', achievements: [] }); }, // 导出PDF async exportPDF() { // 使用jsPDF或调用服务器端生成PDF const response = await fetch('/wp-json/resume-builder/v1/export-pdf', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': resumeBuilderData.nonce }, body: JSON.stringify({ resume: this.resume }) }); const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = '我的简历.pdf'; a.click(); }, // 查找匹配职位 async findMatches() { this.isLoading = true; try { const response = await fetch('/wp-json/resume-builder/v1/find-matches', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': resumeBuilderData.nonce }, body: JSON.stringify({ resume: this.resume }) }); const matches = await response.json(); this.displayMatches(matches); } catch (error) { console.error('匹配错误:', error); alert('匹配过程中发生错误'); } finally { this.isLoading = false; } }, displayMatches(matches) { // 显示匹配结果 const matchModal = new bootstrap.Modal(document.getElementById('matchModal')); this.matchResults = matches; matchModal.show(); } } }); 四、智能职位匹配引擎 4.1 匹配算法设计 智能匹配引擎采用多维度评分算法: <?php class Job_Matching_Engine { private $resume_data; private $job_data; public function __construct($resume_data, $job_data) { $this->resume_data = $resume_data; $this->job_data = $job_data; } // 计算总体匹配分数 public function calculate_match_score() { $weights = array( 'skills' => 0.35, 'experience' => 0.25, 'education' => 0.15, 'location' => 0.10, 'job_type' => 0.10, 'salary' => 0.05 ); $scores = array( 'skills' => $this->match_skills(), 'experience' => $this->match_experience(), 'education' => $this->match_education(), 'location' => $this->match_location(), 'job_type' => $this->match_job_type(), 'salary' => $this->match_salary_expectations() ); // 计算加权总分 $total_score = 0; foreach ($weights as $key => $weight) { $total_score += $scores[$key] * $weight; } return array( 'total_score' => round($total_score, 2), 'category_scores' => $scores, 'match_reasons' => $this->generate_match_reasons($scores) ); } // 技能匹配算法 private function match_skills() { $resume_skills = $this->extract_skills_from_resume(); $job_skills = $this->extract_skills_from_job(); if (empty($job_skills)) { return 50; // 如果没有指定技能要求,给基准分 } $matched_skills = array_intersect($resume_skills, $job_skills); $match_percentage = count($matched_skills) / count($job_skills) * 100; // 确保不超过100分 return min($match_percentage, 100); } // 从简历中提取技能关键词 private function extract_skills_from_resume() { $skills = array(); // 从技能部分提取 if (!empty($this->resume_data['skills']['technical'])) { $skills = array_merge($skills, $this->resume_data['skills']['technical']); } // 从工作经历描述中提取技能关键词 if (!empty($this->resume_data['experience'])) { foreach ($this->resume_data['experience'] as $experience) { $keywords = $this->extract_keywords($experience['description']); $skills = array_merge($skills, $keywords); } } // 标准化技能名称 $skills = $this->normalize_skills($skills); return array_unique($skills); } // 从职位描述中提取技能要求 private function extract_skills_from_job() { $text = $this->job_data['job_description'] . ' ' . $this->job_data['requirements']; // 预定义的技能词典 $skill_dictionary = array( 'PHP', 'JavaScript', 'Python', 'Java', 'WordPress', 'React', 'Vue.js', 'MySQL', 'CSS', 'HTML', 'Git', 'REST API', 'AWS', 'Docker' ); $found_skills = array(); foreach ($skill_dictionary as $skill) { if (stripos($text, $skill) !== false) { $found_skills[] = $skill; } } return array_unique($found_skills); } // 工作经历匹配 private function match_experience() { $required_years = $this->extract_experience_requirement(); $actual_years = $this->calculate_experience_years(); if ($required_years == 0) { return 100; // 不要求工作经验 } if ($actual_years >= $required_years) { return 100; // 完全满足 } else { return ($actual_years / $required_years) * 100; } } // 更多匹配算法... // 生成匹配原因 private function generate_match_reasons($scores) { $reasons = array(); if ($scores['skills'] >= 80) { $reasons[] = '您的技能与职位要求高度匹配'; } elseif ($scores['skills'] >= 50) { $reasons[] = '您具备部分所需技能'; } if ($scores['experience'] >= 90) { $reasons[] = '您的工作经验完全满足职位要求'; } // 添加更多原因... return $reasons; } } ?> 4.2 REST API端点实现 创建WordPress REST API端点处理匹配请求: <?php // 注册REST API路由 add_action('rest_api_init', function() { // 保存简历 register_rest_route('resume-builder/v1', '/save', array( 'methods' => 'POST', 'callback' => 'resume_builder_save_resume', 'permission_callback' => function() { return is_user_logged_in(); } )); // 查找匹配职位 register_rest_route('resume-builder/v1', '/find-matches', array( 'methods' => 'POST', 'callback' => 'resume_builder_find_matches', 'permission_callback' => function() { return is_user_logged_in(); } )); // 获取推荐职位 register_rest_route('resume-builder/v1', '/recommended-jobs', array( 'methods' => 'GET', 'callback' => 'resume_builder_get_recommended_jobs', 'permission_callback' => function() { return is_user_logged_in(); } )); }); // 查找匹配职位回调函数 function resume_builder_find_matches($request) { $user_id = get_current_user_id(); $resume_data = json_decode($request->get_param('resume'), true); // 获取所有活跃职位 global $wpdb; $jobs_table = $wpdb->prefix . 'resume_builder_jobs'; $active_jobs = $wpdb->get_results( "SELECT * FROM {$jobs_table} WHERE is_active = 1 AND expiry_date > NOW()" ); $matches = array(); foreach ($active_jobs as $job) { $matching_engine = new Job_Matching_Engine($resume_data, (array)$job); $match_result = $matching_engine->calculate_match_score(); // 只返回匹配度高于阈值的职位 if ($match_result['total_score'] >= 60) { $matches[] = array( 'job_id' => $job->job_id, 'job_title' => $job->job_title, 'company' => get_company_name($job->company_id), 'location' => $job->location, 'job_type' => $job->job_type, 'match_score' => $match_result['total_score'], 'match_reasons' => $match_result['match_reasons'], 'job_link' => get_permalink($job->job_id) ); } } // 按匹配分数排序 usort($matches, function($a, $b) { return $b['match_score'] <=> $a['match_score']; }); // 保存匹配记录 save_match_records($user_id, $matches); return rest_ensure_response(array( 'success' => true, 'matches' => array_slice($matches, 0, 20), // 返回前20个匹配 'total_found' => count($matches) )); } // 获取推荐职位 function resume_builder_get_recommended_jobs($request) { $user_id = get_current_user_id(); $page = $request->get_param('page') ?: 1; $per_page = 10; $offset = ($page - 1) * $per_page; // 获取用户最新简历 $resumes = get_user_resumes($user_id); if (empty($resumes)) { return rest_ensure_response(array( 'success' => true, 'jobs' => array(), 'message' => '请先创建简历' )); } $latest_resume = json_decode($resumes[0]->resume_data, true); // 从匹配记录中获取推荐职位 global $wpdb; $matches_table = $wpdb->prefix . 'resume_builder_matches'; $jobs_table = $wpdb->prefix . 'resume_builder_jobs'; $recommended_jobs = $wpdb->get_results($wpdb->prepare( "SELECT j.*, m.match_score FROM {$matches_table} m JOIN {$jobs_table} j ON m.job_id = j.job_id WHERE m.resume_id = %d AND j.is_active = 1 ORDER BY m.match_score DESC LIMIT %d OFFSET %d", $resumes[0]->resume_id, $per_page, $offset )); $formatted_jobs = array_map(function($job) { return array( 'id' => $job->job_id, 'title' => $job->job_title, 'company' => get_company_name($job->company_id), 'location' => $job->location, 'type' => $job->job_type, 'salary' => $job->salary_range, 'match_score' => $job->match_score, 'posted_date' => human_time_diff(strtotime($job->posted_date), current_time('timestamp')) . '前', 'apply_link' => home_url('/apply/?job_id=' . $job->job_id) ); }, $recommended_jobs); return rest_ensure_response(array( 'success' => true, 'jobs' => $formatted_jobs, 'pagination' => array( 'page' => $page, 'per_page' => $per_page, 'has_more' => count($recommended_jobs) === $per_page ) )); } ?> 五、用户界面与体验优化 5.1 响应式仪表板设计 创建用户友好的仪表板界面: <?php // 短代码显示用户仪表板 add_shortcode('resume_dashboard', 'resume_builder_dashboard_shortcode'); function resume_builder_dashboard_shortcode() { if (!is_user_logged_in()) { return '<div class="resume-login-required"> <p>请先登录以访问简历中心</p> <a href="' . wp_login_url(get_permalink()) . '" class="btn btn-primary">登录</a> <a href="' . wp_registration_url() . '" class="btn btn-secondary">注册</a> </div>'; } ob_start(); ?> <div class="resume-dashboard"> <div class="dashboard-header"> <h1>简历中心</h1> <div class="dashboard-stats"> <div class="stat-card"> <span class="stat-number"><?php echo count_user_resumes(); ?></span> <span class="stat-label">份简历</span> </div> <div class="stat-card"> <span class="stat-number"><?php echo count_job_matches(); ?></span> <span class="stat-label">个匹配职位</span> </div> <div class="stat-card"> <span class="stat-number"><?php echo count_applications(); ?></span> <span class="stat-label">次申请</span> </div> </div> </div> <div class="dashboard-content"> <div class="dashboard-sidebar"> <nav class="dashboard-nav"> <a href="#my-resumes" class="nav-item active"> <i class="fas fa-file-alt"></i> <span>我的简历</span> </a> <a href="#job-matches" class="nav-item"> <i class="fas fa-bullseye"></i> <span>职位匹配</span> </a> <a href="#applications" class="nav-item"> <i class="fas fa-paper-plane"></i> <span>我的申请</span> </a> <a href="#profile" class="nav-item"> <i class="fas fa-user-cog"></i> <span>账户设置</span> </a> </nav> <div class="quick-actions"> <button class="btn btn-primary btn-block" onclick="window.location.href='<?php echo home_url('/create-resume/'); ?>'"> <i class="fas fa-plus"></i> 创建新简历 </button> <button class="btn btn-secondary btn-block" id="quick-match-btn"> <i class="fas fa-search"></i> 快速匹配职位 </button> </div> </div> <div class="dashboard-main"> <div class="dashboard-section active" id="my-resumes-section"> <h2>我的简历</h2> <div class="resumes-grid"> <?php display_user_resumes(); ?> </div> </div> <div class="dashboard-section" id="job-matches-section"> <h2>推荐职位</h2> <div class="job-filters"> <select id="match-score-filter"> <option value="all">所有匹配度</option> <option value="90">90%以上</option> <option value="80">80%以上</option> <option value="70">70%以上</option> </select> <select id="job-type-filter"> <option value="all">所有类型</option> <option value="full-time">全职</option> <option value="part-time">兼职</option> <option value="remote">远程</option> </select> </div> <div id="job-matches-container"> <!-- 通过AJAX加载职位 --> <div class="loading-spinner"></div> </div> </div> <!-- 其他部分... --> </div> </div> </div> <script> jQuery(document).ready(function($) { // 加载推荐职位 function loadRecommendedJobs(page = 1) { $('#job-matches-container').html('<div class="loading-spinner"></div>'); $.ajax({ url: '<?php echo rest_url('resume-builder/v1/recommended-jobs'); ?>', method: 'GET', data: { page: page }, beforeSend: function(xhr) { xhr.setRequestHeader('X-WP-Nonce', '<?php echo wp_create_nonce('wp_rest'); ?>'); }, success: function(response) { if (response.success) { displayJobs(response.jobs); setupPagination(response.pagination); } } }); } function displayJobs(jobs) { let html = ''; if (jobs.length === 0) { html = '<div class="no-results"><p>暂无推荐职位,请完善您的简历</p></div>'; } else { jobs.forEach(function(job) { html += ` <div class="job-card" data-match="${job.match_score}"> <div class="job-card-header"> <h3>${job.title}</h3> <span class="match-badge">${job.match_score}% 匹配</span> </div> <div class="job-card-body"> <p><i class="fas fa-building"></i> ${job.company}</p> <p><i class="fas fa-map-marker-alt"></i> ${job.location}</p> <p><i class="fas fa-clock"></i> ${job.type}</p> <p><i class="fas fa-money-bill-wave"></i> ${job.salary || '面议'}</p> </div> <div class="job-card-footer"> <span class="posted-date">${job.posted_date}</span> <a href="${job.apply_link}" class="btn btn-apply">立即申请</a> </div> </div> `; }); } $('#job-matches-container').html(html); } // 初始加载 loadRecommendedJobs(); // 筛选功能 $('#match-score-filter, #job-type-filter').change(function() { applyFilters(); }); function applyFilters() { const minScore = $('#match-score-filter').val(); const jobType = $('#job-type-filter').val(); $('.job-card').each(function() { const matchScore = parseInt($(this).data('match')); const cardJobType = $(this).find('.fa-clock').next().text().trim(); let show = true; if (minScore !== 'all' && matchScore < parseInt(minScore)) { show = false; } if (jobType !== 'all' && !cardJobType.includes(jobType === 'full-time' ? '全职' : jobType === 'part-time' ? '兼职' : '远程')) { show = false; } $(this).toggle(show); }); } }); </script> <?php return ob_get_clean(); } ?> 5.2 简历模板系统 实现多套简历模板供用户选择: <?php class Resume_Templates { private static $templates = array( 'modern' => array( 'name' => '现代简约', 'description' => '简洁专业的现代风格', 'preview' => RESUME_BUILDER_URL . 'templates/modern/preview.jpg', 'styles' => array( 'primary_color' => '#2563eb', 'secondary_color' => '#1e40af', 'font_family' => 'Segoe UI, sans-serif', 'layout' => 'single-column' ) ), 'classic' => array( 'name' => '经典传统', 'description' => '传统保守的商务风格', 'preview' => RESUME_BUILDER_URL . 'templates/classic/preview.jpg', 'styles' => array( 'primary_color' => '#374151', 'secondary_color' => '#111827', 'font_family' => 'Times New Roman, serif', 'layout' => 'two-column' ) ), 'creative' => array( 'name' => '创意设计', 'description' => '适合创意行业的个性风格', 'preview' => RESUME_BUILDER_URL . 'templates/creative/preview.jpg', 'styles' => array( 'primary_color' => '#7c3aed', 'secondary_color' => '#5b21b6', 'font_family' => 'Inter, sans-serif', 'layout' => 'creative' ) ) ); public static function get_all_templates() { return apply_filters('resume_builder_templates', self::$templates); } public static function render_resume($resume_data, $template_name = 'modern') { $templates = self::get_all_templates(); if (!isset($templates[$template_name])) { $template_name = 'modern'; } $template = $templates[$template_name]; ob_start(); ?> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title><?php echo esc_html($resume_data['personal']['name']); ?>的简历</title> <style> :root { --primary-color: <?php echo $template['styles']['primary_color']; ?>; --secondary-color: <?php echo $template['styles']['secondary_color']; ?>; --font-family: <?php echo $template['styles']['font_family']; ?>; } body { font-family: var(--font-family); line-height: 1.6; color: #333; max-width: 210mm; margin: 0 auto; padding: 20px; background: #fff; } .resume-header { border-bottom: 3px solid var(--primary-color); padding-bottom: 20px; margin-bottom: 30px; } .resume-header h1 { color: var(--primary-color); margin: 0; font-size: 32px; } .resume-header h2 { color: var(--secondary-color); margin: 5px 0 15px; font-size: 20px; font-weight: normal; } .contact-info { display: flex; flex-wrap: wrap; gap: 15px; } .contact-item { display: flex; align-items: center; gap: 5px; } .section { margin-bottom: 25px; } .section-title { color: var(--primary-color); border-bottom: 2px solid var(--secondary-color); padding-bottom: 5px; margin-bottom: 15px; font-size: 18px; } .experience-item, .education-item { margin-bottom: 20px; } .item-header { display: flex; justify-content: space-between; margin-bottom: 5px; } .item-title { font-weight: bold; font-size: 16px; } .item-subtitle { color: var(--secondary-color); font-style: italic; } .item-date { color: #666; } .skills-list { display: flex; flex-wrap: wrap; gap: 10px; } .skill-tag { background: var(--primary-color); color: white; padding: 5px 12px; border-radius: 20px; font-size: 14px; } @media print { body { padding: 0; } .no-print { display: none; } } </style> </head> <body> <div class="resume-container"> <!-- 头部信息 --> <div class="resume-header"> <h1><?php echo esc_html($resume_data['personal']['name']); ?></h1> <h2><?php echo esc_html($resume_data['personal']['title']); ?></h2> <div class="contact-info"> <?php if (!empty($resume_data['personal']['email'])): ?> <div class="contact-item"> <i class="fas fa-envelope"></i> <span><?php echo esc_html($resume_data['personal']['email']); ?></span> </div> <?php endif; ?> <?php if (!empty($resume_data['personal']['phone'])): ?> <div class="contact-item"> <i class="fas fa-phone"></i> <span><?php echo esc_html($resume_data['personal']['phone']); ?></span>
发表评论手把手教学:为你的网站添加在线协同白板与团队创意协作功能 引言:为什么你的网站需要协同白板功能? 在当今数字化工作环境中,远程协作已成为常态。无论是产品设计、项目规划还是创意头脑风暴,团队成员往往分散在不同地点。传统的沟通方式如邮件、即时消息和视频会议虽然有效,但在创意协作方面存在明显局限——缺乏直观的视觉共享空间。 在线协同白板正是解决这一痛点的理想工具。它提供了一个虚拟的"画布",团队成员可以实时绘制图表、添加便签、上传图片、创建思维导图,并看到彼此的修改。这种视觉化协作方式能显著提高团队效率,激发创意灵感,并确保所有参与者对项目有统一的理解。 对于WordPress网站所有者来说,添加这样的功能不仅能提升用户体验,还能将你的网站从一个单向信息发布平台转变为互动协作空间。无论是企业内部协作、在线教育、咨询服务还是客户项目沟通,协同白板都能为你的网站增加巨大价值。 第一部分:准备工作与环境配置 1.1 理解WordPress二次开发基础 在开始之前,我们需要明确WordPress二次开发的基本概念。WordPress不仅是一个内容管理系统,更是一个强大的开发平台,通过其丰富的API和钩子系统,我们可以扩展其功能而不影响核心文件。 关键概念: 主题与插件:功能扩展主要通过子主题或自定义插件实现 动作钩子(Action Hooks):在特定时间点执行自定义代码 过滤器钩子(Filter Hooks):修改WordPress处理的数据 短代码(Shortcodes):在内容中嵌入动态功能 REST API:为前端应用提供数据接口 1.2 开发环境搭建 为了安全地进行开发,我们首先需要搭建本地测试环境: 本地服务器环境:推荐使用XAMPP、MAMP或Local by Flywheel 代码编辑器:VS Code、PHPStorm或Sublime Text 浏览器开发者工具:Chrome DevTools或Firefox Developer Tools 版本控制:Git用于代码管理 1.3 创建自定义插件框架 我们将创建一个独立的插件来管理所有协同白板功能,这样可以确保功能独立于主题,便于维护和迁移。 <?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('COLLAB_WHITEBOARD_VERSION', '1.0.0'); define('COLLAB_WHITEBOARD_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('COLLAB_WHITEBOARD_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 function collab_whiteboard_init() { // 检查依赖项 if (!class_exists('WP_List_Table')) { require_once(ABSPATH . 'wp-admin/includes/class-wp-list-table.php'); } // 加载必要文件 require_once COLLAB_WHITEBOARD_PLUGIN_DIR . 'includes/class-whiteboard-manager.php'; require_once COLLAB_WHITEBOARD_PLUGIN_DIR . 'includes/class-whiteboard-db.php'; require_once COLLAB_WHITEBOARD_PLUGIN_DIR . 'includes/class-whiteboard-shortcodes.php'; // 初始化组件 new Whiteboard_Manager(); new Whiteboard_Shortcodes(); } add_action('plugins_loaded', 'collab_whiteboard_init'); // 激活插件时创建数据库表 function collab_whiteboard_activate() { require_once COLLAB_WHITEBOARD_PLUGIN_DIR . 'includes/class-whiteboard-db.php'; Whiteboard_DB::create_tables(); } register_activation_hook(__FILE__, 'collab_whiteboard_activate'); // 停用插件时的清理工作 function collab_whiteboard_deactivate() { // 可选的清理代码 } register_deactivation_hook(__FILE__, 'collab_whiteboard_deactivate'); 第二部分:数据库设计与用户权限管理 2.1 设计协同白板数据库结构 协同白板需要存储白板数据、用户权限和修改历史。我们创建以下数据库表: // 在includes/class-whiteboard-db.php中 class Whiteboard_DB { public static function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_prefix = $wpdb->prefix . 'collab_'; // 白板主表 $whiteboards_table = $table_prefix . 'whiteboards'; $sql1 = "CREATE TABLE IF NOT EXISTS $whiteboards_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, title varchar(255) NOT NULL, description text, content longtext, created_by bigint(20) NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, settings text, is_public tinyint(1) DEFAULT 0, PRIMARY KEY (id) ) $charset_collate;"; // 白板权限表 $permissions_table = $table_prefix . 'permissions'; $sql2 = "CREATE TABLE IF NOT EXISTS $permissions_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, whiteboard_id mediumint(9) NOT NULL, user_id bigint(20) NOT NULL, permission_level varchar(50) NOT NULL, added_by bigint(20) NOT NULL, added_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY unique_whiteboard_user (whiteboard_id, user_id) ) $charset_collate;"; // 白板历史记录表 $history_table = $table_prefix . 'history'; $sql3 = "CREATE TABLE IF NOT EXISTS $history_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, whiteboard_id mediumint(9) NOT NULL, user_id bigint(20) NOT NULL, action varchar(100) NOT NULL, changes text, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY whiteboard_id (whiteboard_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql1); dbDelta($sql2); dbDelta($sql3); } } 2.2 实现用户权限系统 协同白板需要精细的权限控制,不同用户应有不同级别的访问和编辑权限: class Whiteboard_Permissions { const PERMISSION_VIEW = 'view'; const PERMISSION_COMMENT = 'comment'; const PERMISSION_EDIT = 'edit'; const PERMISSION_ADMIN = 'admin'; /** * 检查用户对白板的权限 */ public static function check_permission($whiteboard_id, $user_id, $required_permission) { global $wpdb; // 获取白板信息 $table_name = $wpdb->prefix . 'collab_whiteboards'; $whiteboard = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $whiteboard_id )); if (!$whiteboard) { return false; } // 如果是公开白板且只需要查看权限 if ($whiteboard->is_public && $required_permission === self::PERMISSION_VIEW) { return true; } // 创建者拥有所有权限 if ($whiteboard->created_by == $user_id) { return true; } // 检查用户特定权限 $permissions_table = $wpdb->prefix . 'collab_permissions'; $user_permission = $wpdb->get_var($wpdb->prepare( "SELECT permission_level FROM $permissions_table WHERE whiteboard_id = %d AND user_id = %d", $whiteboard_id, $user_id )); if (!$user_permission) { return false; } // 权限等级映射 $permission_hierarchy = [ self::PERMISSION_VIEW => 1, self::PERMISSION_COMMENT => 2, self::PERMISSION_EDIT => 3, self::PERMISSION_ADMIN => 4 ]; $required_level = $permission_hierarchy[$required_permission] ?? 0; $user_level = $permission_hierarchy[$user_permission] ?? 0; return $user_level >= $required_level; } /** * 为用户分配白板权限 */ public static function assign_permission($whiteboard_id, $user_id, $permission_level, $assigned_by) { global $wpdb; $table_name = $wpdb->prefix . 'collab_permissions'; // 检查是否已存在权限记录 $existing = $wpdb->get_var($wpdb->prepare( "SELECT id FROM $table_name WHERE whiteboard_id = %d AND user_id = %d", $whiteboard_id, $user_id )); if ($existing) { // 更新现有权限 return $wpdb->update( $table_name, [ 'permission_level' => $permission_level, 'added_by' => $assigned_by ], [ 'whiteboard_id' => $whiteboard_id, 'user_id' => $user_id ] ); } else { // 插入新权限记录 return $wpdb->insert( $table_name, [ 'whiteboard_id' => $whiteboard_id, 'user_id' => $user_id, 'permission_level' => $permission_level, 'added_by' => $assigned_by ] ); } } } 第三部分:前端白板界面与实时协作实现 3.1 选择前端绘图库 对于协同白板,我们需要一个强大的前端绘图库。这里我们选择Fabric.js,它是一个功能强大的Canvas库,支持丰富的图形操作和事件处理。 首先,在插件中注册必要的脚本和样式: class Whiteboard_Assets { public static function enqueue_frontend_assets() { // Fabric.js 绘图库 wp_enqueue_script( 'fabric-js', 'https://cdnjs.cloudflare.com/ajax/libs/fabric.js/4.5.0/fabric.min.js', [], '4.5.0', true ); // Socket.io 客户端 (用于实时通信) wp_enqueue_script( 'socket-io', 'https://cdn.socket.io/4.5.0/socket.io.min.js', [], '4.5.0', true ); // 自定义白板脚本 wp_enqueue_script( 'collab-whiteboard', COLLAB_WHITEBOARD_PLUGIN_URL . 'assets/js/whiteboard.js', ['jquery', 'fabric-js', 'socket-io'], COLLAB_WHITEBOARD_VERSION, true ); // 白板样式 wp_enqueue_style( 'collab-whiteboard-style', COLLAB_WHITEBOARD_PLUGIN_URL . 'assets/css/whiteboard.css', [], COLLAB_WHITEBOARD_VERSION ); // 本地化脚本,传递必要数据 wp_localize_script('collab-whiteboard', 'whiteboardConfig', [ 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('collab_whiteboard_nonce'), 'currentUserId' => get_current_user_id(), 'socketServer' => self::get_socket_server_url() ]); } private static function get_socket_server_url() { // 这里返回你的WebSocket服务器地址 // 可以是独立的Node.js服务器或通过WordPress REST API实现 return home_url('/wp-json/collab-whiteboard/v1/socket'); } } add_action('wp_enqueue_scripts', ['Whiteboard_Assets', 'enqueue_frontend_assets']); 3.2 创建白板画布界面 接下来,我们创建白板的主要HTML结构和JavaScript逻辑: <!-- 在短代码输出的HTML中 --> <div class="whiteboard-container" data-whiteboard-id="<?php echo $whiteboard_id; ?>"> <div class="whiteboard-toolbar"> <div class="tool-group"> <button class="tool-btn" data-tool="select" title="选择工具"> <i class="icon-cursor"></i> </button> <button class="tool-btn active" data-tool="pencil" title="铅笔"> <i class="icon-pencil"></i> </button> <button class="tool-btn" data-tool="line" title="直线"> <i class="icon-line"></i> </button> <button class="tool-btn" data-tool="rectangle" title="矩形"> <i class="icon-square"></i> </button> <button class="tool-btn" data-tool="circle" title="圆形"> <i class="icon-circle"></i> </button> <button class="tool-btn" data-tool="text" title="文本"> <i class="icon-text"></i> </button> </div> <div class="tool-group"> <input type="color" class="color-picker" value="#000000" title="颜色"> <input type="range" class="brush-size" min="1" max="50" value="3" title="笔刷大小"> <button class="tool-btn" data-action="undo" title="撤销"> <i class="icon-undo"></i> </button> <button class="tool-btn" data-action="redo" title="重做"> <i class="icon-redo"></i> </button> <button class="tool-btn" data-action="clear" title="清空白板"> <i class="icon-trash"></i> </button> </div> <div class="tool-group user-list"> <span class="online-users">在线用户: <span class="user-count">1</span></span> </div> </div> <div class="whiteboard-canvas-container"> <canvas id="whiteboard-canvas"></canvas> </div> <div class="whiteboard-sidebar"> <div class="sidebar-section"> <h4>元素属性</h4> <div class="properties-panel"> <!-- 动态属性控件将在这里显示 --> </div> </div> <div class="sidebar-section"> <h4>聊天与评论</h4> <div class="chat-container"> <div class="chat-messages"></div> <div class="chat-input"> <input type="text" placeholder="输入消息..."> <button class="send-btn">发送</button> </div> </div> </div> </div> </div> 3.3 实现实时协作功能 实时协作是协同白板的核心功能。我们使用WebSocket实现实时数据同步: // assets/js/whiteboard.js class CollaborativeWhiteboard { constructor(containerElement) { this.container = containerElement; this.whiteboardId = containerElement.dataset.whiteboardId; this.canvas = null; this.socket = null; this.currentTool = 'pencil'; this.isDrawing = false; this.lastPoint = null; this.history = []; this.historyIndex = -1; this.init(); } init() { // 初始化画布 this.canvas = new fabric.Canvas('whiteboard-canvas', { isDrawingMode: true, width: this.container.querySelector('.whiteboard-canvas-container').offsetWidth, height: 600, backgroundColor: '#ffffff' }); // 连接WebSocket服务器 this.connectSocket(); // 绑定事件 this.bindEvents(); // 加载现有白板数据 this.loadWhiteboardData(); } connectSocket() { // 连接到WebSocket服务器 this.socket = io(whiteboardConfig.socketServer, { query: { whiteboardId: this.whiteboardId, userId: whiteboardConfig.currentUserId } }); // 监听服务器消息 this.socket.on('connect', () => { console.log('已连接到白板服务器'); }); this.socket.on('drawing', (data) => { this.handleRemoteDrawing(data); }); this.socket.on('object:modified', (data) => { this.handleRemoteObjectModification(data); }); this.socket.on('user:joined', (data) => { this.updateOnlineUsers(data.users); }); this.socket.on('user:left', (data) => { this.updateOnlineUsers(data.users); }); this.socket.on('chat:message', (data) => { this.addChatMessage(data); }); } bindEvents() { // 工具按钮点击事件 this.container.querySelectorAll('.tool-btn[data-tool]').forEach(btn => { btn.addEventListener('click', (e) => { this.setTool(e.target.closest('.tool-btn').dataset.tool); }); }); // 动作按钮点击事件 this.container.querySelectorAll('.tool-btn[data-action]').forEach(btn => { btn.addEventListener('click', (e) => { const action = e.target.closest('.tool-btn').dataset.action; this.handleAction(action); }); }); // 颜色选择器 EventListener('change', (e) => { this.setColor(e.target.value); }); // 笔刷大小 this.container.querySelector('.brush-size').addEventListener('input', (e) => { this.setBrushSize(parseInt(e.target.value)); }); // 画布事件 this.canvas.on('mouse:down', (options) => { this.onMouseDown(options); }); this.canvas.on('mouse:move', (options) => { this.onMouseMove(options); }); this.canvas.on('mouse:up', (options) => { this.onMouseUp(options); }); this.canvas.on('object:added', (options) => { this.onObjectAdded(options); }); this.canvas.on('object:modified', (options) => { this.onObjectModified(options); }); // 聊天功能 const chatInput = this.container.querySelector('.chat-input input'); const sendBtn = this.container.querySelector('.chat-input .send-btn'); sendBtn.addEventListener('click', () => { this.sendChatMessage(chatInput.value); chatInput.value = ''; }); chatInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { this.sendChatMessage(chatInput.value); chatInput.value = ''; } }); // 窗口大小调整 window.addEventListener('resize', () => { this.resizeCanvas(); }); } setTool(tool) { this.currentTool = tool; // 更新按钮状态 this.container.querySelectorAll('.tool-btn[data-tool]').forEach(btn => { btn.classList.toggle('active', btn.dataset.tool === tool); }); // 根据工具设置画布模式 switch(tool) { case 'select': this.canvas.isDrawingMode = false; this.canvas.selection = true; break; case 'pencil': this.canvas.isDrawingMode = true; this.canvas.freeDrawingBrush = new fabric.PencilBrush(this.canvas); this.canvas.freeDrawingBrush.width = this.brushSize || 3; this.canvas.freeDrawingBrush.color = this.currentColor || '#000000'; break; case 'line': this.canvas.isDrawingMode = false; this.canvas.selection = false; // 实现直线绘制逻辑 break; // 其他工具的实现... } } setColor(color) { this.currentColor = color; if (this.canvas.isDrawingMode) { this.canvas.freeDrawingBrush.color = color; } // 如果选择了对象,则更改对象颜色 const activeObject = this.canvas.getActiveObject(); if (activeObject) { activeObject.set('fill', color); this.canvas.renderAll(); this.sendObjectUpdate(activeObject); } } setBrushSize(size) { this.brushSize = size; if (this.canvas.isDrawingMode) { this.canvas.freeDrawingBrush.width = size; } } onMouseDown(options) { if (!options.target && this.currentTool === 'line') { this.isDrawing = true; this.lastPoint = options.pointer; } } onMouseMove(options) { if (this.isDrawing && this.currentTool === 'line') { // 绘制直线预览 } } onMouseUp(options) { if (this.isDrawing && this.currentTool === 'line' && this.lastPoint) { const line = new fabric.Line([ this.lastPoint.x, this.lastPoint.y, options.pointer.x, options.pointer.y ], { stroke: this.currentColor || '#000000', strokeWidth: this.brushSize || 3 }); this.canvas.add(line); this.isDrawing = false; this.lastPoint = null; } } onObjectAdded(options) { // 保存到历史记录 this.saveToHistory(); // 发送到服务器 if (options.target) { this.sendDrawingData({ type: 'object:added', object: options.target.toJSON(), userId: whiteboardConfig.currentUserId }); } } onObjectModified(options) { // 发送对象更新到服务器 if (options.target) { this.sendObjectUpdate(options.target); } } sendDrawingData(data) { if (this.socket && this.socket.connected) { this.socket.emit('drawing', { whiteboardId: this.whiteboardId, ...data }); } } sendObjectUpdate(object) { this.sendDrawingData({ type: 'object:modified', object: object.toJSON(), userId: whiteboardConfig.currentUserId }); } handleRemoteDrawing(data) { // 忽略自己发送的数据 if (data.userId === whiteboardConfig.currentUserId) return; switch(data.type) { case 'object:added': fabric.util.enlivenObjects([data.object], (objects) => { objects.forEach(obj => { this.canvas.add(obj); }); }); break; case 'object:modified': const object = this.canvas.getObjects().find(obj => obj.data && obj.data.id === data.object.data.id ); if (object) { object.set(data.object); this.canvas.renderAll(); } break; } } sendChatMessage(message) { if (!message.trim()) return; if (this.socket && this.socket.connected) { this.socket.emit('chat:message', { whiteboardId: this.whiteboardId, userId: whiteboardConfig.currentUserId, message: message, timestamp: new Date().toISOString() }); } } addChatMessage(data) { const chatMessages = this.container.querySelector('.chat-messages'); const messageElement = document.createElement('div'); messageElement.className = 'chat-message'; messageElement.innerHTML = ` <div class="message-header"> <span class="user-name">用户 ${data.userId}</span> <span class="message-time">${new Date(data.timestamp).toLocaleTimeString()}</span> </div> <div class="message-content">${this.escapeHtml(data.message)}</div> `; chatMessages.appendChild(messageElement); chatMessages.scrollTop = chatMessages.scrollHeight; } updateOnlineUsers(users) { const userCountElement = this.container.querySelector('.user-count'); if (userCountElement) { userCountElement.textContent = users.length; } } saveToHistory() { // 保存当前画布状态到历史记录 const state = JSON.stringify(this.canvas.toJSON()); this.history = this.history.slice(0, this.historyIndex + 1); this.history.push(state); this.historyIndex++; } loadWhiteboardData() { // 通过AJAX加载白板数据 jQuery.ajax({ url: whiteboardConfig.ajaxUrl, method: 'POST', data: { action: 'load_whiteboard', whiteboard_id: this.whiteboardId, nonce: whiteboardConfig.nonce }, success: (response) => { if (response.success && response.data) { this.canvas.loadFromJSON(response.data, () => { this.canvas.renderAll(); }); } } }); } resizeCanvas() { const container = this.container.querySelector('.whiteboard-canvas-container'); this.canvas.setDimensions({ width: container.offsetWidth, height: 600 }); } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } } // 初始化白板document.addEventListener('DOMContentLoaded', () => { const whiteboardContainers = document.querySelectorAll('.whiteboard-container'); whiteboardContainers.forEach(container => { new CollaborativeWhiteboard(container); }); }); ## 第四部分:后端WebSocket服务器与数据同步 ### 4.1 设置WebSocket服务器 对于实时协作,我们需要一个WebSocket服务器来处理客户端连接和数据广播。这里我们使用Node.js和Socket.io: // server/whiteboard-server.jsconst http = require('http');const socketIo = require('socket.io');const mysql = require('mysql2/promise'); // 创建HTTP服务器const server = http.createServer();const io = socketIo(server, { cors: { origin: process.env.WORDPRESS_URL || "http://localhost", methods: ["GET", "POST"] } }); // 数据库连接池const dbPool = mysql.createPool({ host: process.env.DB_HOST || 'localhost', user: process.env.DB_USER || 'wordpress_user', password: process.env.DB_PASSWORD || 'password', database: process.env.DB_NAME || 'wordpress_db', waitForConnections: true, connectionLimit: 10, queueLimit: 0 }); // 存储在线用户const onlineUsers = new Map(); // whiteboardId -> Set of userIdsconst userSockets = new Map(); // userId -> Set of socketIds io.on('connection', (socket) => { const { whiteboardId, userId } = socket.handshake.query; console.log(`用户 ${userId} 连接到白板 ${whiteboardId}`); // 验证用户权限 validateUserPermission(whiteboardId, userId).then(hasPermission => { if (!hasPermission) { socket.disconnect(); return; } // 加入白板房间 socket.join(`whiteboard:${whiteboardId}`); // 更新在线用户列表 if (!onlineUsers.has(whiteboardId)) { onlineUsers.set(whiteboardId, new Set()); } onlineUsers.get(whiteboardId).add(userId); // 存储用户socket映射 if (!userSockets.has(userId)) { userSockets.set(userId, new Set()); } userSockets.get(userId).add(socket.id); // 广播用户加入事件 io.to(`whiteboard:${whiteboardId}`).emit('user:joined', { whiteboardId, userId, users: Array.from(onlineUsers.get(whiteboardId)) }); // 处理绘图事件 socket.on('drawing', (data) => { // 验证数据 if (data.whiteboardId !== whiteboardId) return; // 广播给同一白板的其他用户 socket.to(`whiteboard:${whiteboardId}`).emit('drawing', { ...data, timestamp: new Date().toISOString() }); // 保存到数据库(可选,根据需求) saveDrawingAction(whiteboardId, userId, data); }); // 处理聊天消息 socket.on('chat:message', (data) => { if (data.whiteboardId !== whiteboardId) return; // 广播聊天消息 io.to(`whiteboard:${whiteboardId}`).emit('chat:message', { ...data, timestamp: new Date().toISOString() }); // 保存聊天记录到数据库 saveChatMessage(whiteboardId, userId, data.message); }); // 处理断开连接 socket.on('disconnect', () => { console.log(`用户 ${userId} 断开连接`); // 清理用户socket映射 if (userSockets.has(userId)) { userSockets.get(userId).delete(socket.id); if (userSockets.get(userId).size === 0) { userSockets.delete(userId); // 从在线用户中移除 if (onlineUsers.has(whiteboardId)) { onlineUsers.get(whiteboardId).delete(userId); // 广播用户离开事件 io.to(`whiteboard:${whiteboardId}`).emit('user:left', { whiteboardId, userId, users: Array.from(onlineUsers.get(whiteboardId)) }); } } } }); }).catch(error => { console.error('权限验证失败:', error); socket.disconnect(); }); }); // 验证用户权限async function validateUserPermission(whiteboardId, userId) { try { const [rows] = await dbPool.execute( `SELECT w.is_public, p.permission_level FROM wp_collab_whiteboards w LEFT JOIN wp_collab_permissions p ON w.id = p.whiteboard_id AND p.user_id = ? WHERE w.id = ?`, [userId, whiteboardId] ); if (rows.length === 0) return false; const whiteboard = rows[0]; // 检查权限 if (whiteboard.is_public) return true; if (whiteboard.permission_level) return true; return false; } catch (error) { console.error('数据库查询错误:', error); return false; } } // 保存绘图动作到历史记录async function saveDrawingAction(whiteboardId, userId, data) { try { await dbPool.execute( `INSERT INTO wp_collab_history (whiteboard_id, user_id, action, changes) VALUES (?, ?, ?, ?)`, [whiteboardId, userId, data.type, JSON.stringify(data)] ); } catch (error) { console.error('保存历史记录失败:', error); } } // 保存聊天消息async function saveChatMessage(whiteboardId, userId, message) { try { await dbPool.execute( `INSERT INTO wp_collab_chat (whiteboard_id, user_id, message) VALUES (?, ?, ?)`, [whiteboardId, userId, message] ); } catch (error) { console.error('保存聊天消息失败:', error); } } // 启动服务器const PORT = process.env.PORT || 3000;server.listen(PORT, () => { console.log(`白板服务器运行在端口 ${PORT}`); }); ### 4.2 WordPress REST API集成 为了让WebSocket服务器与WordPress通信,我们需要创建REST API端点: // includes/class-whiteboard-api.phpclass Whiteboard_API { public function register_routes() { register_rest_route('collab-whiteboard/v1', '/whiteboard/(?P<id>d+)', [ [ 'methods' => 'GET', 'callback' => [$this, 'get_whiteboard'], 'permission_callback' => [$this, 'check_whiteboard_permission'] ], [ 'methods' => 'POST', 'callback' => [$this, 'update_whiteboard'], 'permission_callback' => [$this, 'check_edit_permission'] ] ]); register_rest_route('collab-whiteboard/v1', '/whiteboard/(?P<id>d+)/history', [ [ 'methods' => 'GET', 'callback' => [$this, 'get_whiteboard_history'], 'permission_callback' => [$this, 'check_whiteboard_permission'] ] ]); register_rest_route('collab-whiteboard/v1', '/whiteboard/(?P<id>d+)/chat', [ [ 'methods' => 'GET', 'callback' => [$this, 'get_chat_messages'], 'permission_callback' => [$this, 'check_whiteboard_permission'] ], [ 'methods' => 'POST', 'callback' => [$this, 'post_chat_message'], 'permission_callback' => [$this, 'check_comment_permission'] ] ]); } public function get_whiteboard($request) { $whiteboard_id = $request['id']; global $wpdb; $table_name = $wpdb->prefix . 'collab_whiteboards'; $whiteboard = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $whiteboard_id )); if (!$whiteboard) { return new WP_Error('not_found', '白板不存在', ['status' => 404]); } return rest_ensure_response([ 'id' => $whiteboard->id, 'title' => $whiteboard->title, 'description' => $whiteboard->description, 'content' => json_decode($whiteboard->content, true), 'settings' => json_decode($whiteboard->settings, true), 'created_by' => $whiteboard->created_by, 'created_at' => $whiteboard->created_at, 'updated_at' => $whiteboard->updated_at, 'is_public' => (bool)$whiteboard->is_public ]); } public function update_whiteboard($request) { $whiteboard_id = $request['id']; $content = $request->get_param('content'); if (empty($content)) { return new WP_Error('invalid_data', '内容不能为空', ['status' => 400]); } global $wpdb; $table_name = $wpdb->prefix . 'collab_whiteboards'; $result = $wpdb->update( $table_name, [ 'content' => json_encode($content), 'updated_at' => current_time('mysql') ], ['id' => $whiteboard_id] ); if ($result === false) { return new WP_Error('update_failed', '更新失败', ['status' => 500]); } return rest_ensure_response([ 'success' => true, 'message' => '白板已更新' ]); } public function check_whiteboard_permission($request) { $whiteboard_id = $request['id']; $user_id = get_current_user_id(); return Whiteboard_Permissions::check_permission( $whiteboard_id, $user_id, Whiteboard_Permissions::PERMISSION_VIEW
发表评论WordPress插件开发教程:集成实时翻译聊天与多语言客服工具 引言:WordPress插件开发的无限可能 在当今全球化的互联网环境中,多语言支持已成为网站建设的标配功能。对于WordPress这一占据全球43%网站市场的开源平台而言,通过插件扩展其多语言功能具有巨大的实用价值和商业潜力。本教程将深入讲解如何开发一个集成了实时翻译聊天与多语言客服工具的WordPress插件,通过代码二次开发实现这一实用功能。 WordPress插件开发不仅能够满足特定业务需求,还能为开发者带来可观的收益。根据WordPress官方数据,插件目录中已有超过5.8万个免费插件,而高级插件市场更是价值数十亿美元。掌握插件开发技能,意味着您可以为全球数百万WordPress网站提供解决方案。 第一章:开发环境搭建与基础准备 1.1 开发环境配置 在开始插件开发前,我们需要搭建合适的开发环境: 本地开发环境:推荐使用XAMPP、MAMP或Local by Flywheel,它们提供了完整的PHP、MySQL和Apache/Nginx环境。 代码编辑器:Visual Studio Code、PHPStorm或Sublime Text都是优秀的选择,确保安装PHP智能提示和WordPress代码片段插件。 WordPress安装:下载最新版WordPress并安装在本地环境中,建议使用调试模式,在wp-config.php中添加: define('WP_DEBUG', true); define('WP_DEBUG_LOG', true); define('WP_DEBUG_DISPLAY', false); 1.2 插件基础结构 创建一个标准的WordPress插件需要遵循特定的目录结构和文件组织: multilingual-chat-support/ │ ├── multilingual-chat-support.php # 主插件文件 ├── uninstall.php # 卸载脚本 ├── readme.txt # 插件说明 ├── assets/ # 静态资源 │ ├── css/ │ ├── js/ │ └── images/ ├── includes/ # 包含文件 │ ├── class-chat-handler.php │ ├── class-translation-api.php │ └── class-admin-settings.php ├── languages/ # 国际化文件 ├── templates/ # 前端模板 └── vendor/ # 第三方库(如果需要) 1.3 主插件文件结构 主插件文件是插件的入口点,需要包含标准的插件头部信息: <?php /** * Plugin Name: 多语言实时聊天与客服工具 * Plugin URI: https://yourwebsite.com/multilingual-chat * Description: 为WordPress网站添加实时翻译聊天和多语言客服功能 * Version: 1.0.0 * Author: 您的名称 * Author URI: https://yourwebsite.com * License: GPL v2 or later * Text Domain: multilingual-chat * Domain Path: /languages */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('MCS_VERSION', '1.0.0'); define('MCS_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('MCS_PLUGIN_URL', plugin_dir_url(__FILE__)); define('MCS_PLUGIN_BASENAME', plugin_basename(__FILE__)); // 初始化插件 require_once MCS_PLUGIN_DIR . 'includes/class-plugin-init.php'; $mcs_plugin = new MCS_Plugin_Init(); $mcs_plugin->run(); 第二章:实时翻译功能集成 2.1 翻译API选择与集成 实时翻译是插件的核心功能之一。我们可以选择多种翻译API: Google Cloud Translation API:准确度高,支持100多种语言 Microsoft Azure Translator:性价比优秀,有免费额度 DeepL API:欧洲语言翻译质量极高 百度翻译API:中文翻译效果优秀 以下是如何集成Google翻译API的示例: <?php class MCS_Translation_API { private $api_key; private $api_url = 'https://translation.googleapis.com/language/translate/v2'; public function __construct($api_key) { $this->api_key = $api_key; } /** * 翻译文本 * @param string $text 要翻译的文本 * @param string $target_lang 目标语言代码 * @param string $source_lang 源语言代码(可选,自动检测) * @return array 翻译结果 */ public function translate($text, $target_lang, $source_lang = null) { $args = array( 'key' => $this->api_key, 'q' => $text, 'target' => $target_lang, 'format' => 'text' ); if ($source_lang) { $args['source'] = $source_lang; } $url = add_query_arg($args, $this->api_url); $response = wp_remote_get($url, array( 'timeout' => 15, 'headers' => array( 'Content-Type' => 'application/json', ) )); if (is_wp_error($response)) { return array( 'success' => false, 'error' => $response->get_error_message() ); } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if (isset($data['error'])) { return array( 'success' => false, 'error' => $data['error']['message'] ); } return array( 'success' => true, 'translatedText' => $data['data']['translations'][0]['translatedText'], 'detectedSourceLanguage' => $data['data']['translations'][0]['detectedSourceLanguage'] ); } /** * 批量翻译 */ public function translate_batch($texts, $target_lang, $source_lang = null) { // 实现批量翻译逻辑 } /** * 获取支持的语言列表 */ public function get_supported_languages() { $url = 'https://translation.googleapis.com/language/translate/v2/languages'; $url = add_query_arg(array( 'key' => $this->api_key, 'target' => 'zh-CN' ), $url); // 发送请求并处理响应 } } 2.2 本地缓存机制 为了减少API调用次数和提高响应速度,我们需要实现翻译缓存: class MCS_Translation_Cache { private $cache_table; public function __construct() { global $wpdb; $this->cache_table = $wpdb->prefix . 'mcs_translation_cache'; } /** * 创建缓存表 */ public function create_table() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE IF NOT EXISTS {$this->cache_table} ( id bigint(20) NOT NULL AUTO_INCREMENT, source_text text NOT NULL, source_lang varchar(10) DEFAULT '', target_lang varchar(10) NOT NULL, translated_text text NOT NULL, hash varchar(32) NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY hash (hash), KEY source_lang (source_lang), KEY target_lang (target_lang) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } /** * 获取缓存翻译 */ public function get($text, $target_lang, $source_lang = '') { global $wpdb; $hash = md5($text . $source_lang . $target_lang); $result = $wpdb->get_row($wpdb->prepare( "SELECT translated_text FROM {$this->cache_table} WHERE hash = %s", $hash )); return $result ? $result->translated_text : false; } /** * 设置缓存 */ public function set($text, $translated_text, $target_lang, $source_lang = '') { global $wpdb; $hash = md5($text . $source_lang . $target_lang); $wpdb->insert( $this->cache_table, array( 'source_text' => $text, 'source_lang' => $source_lang, 'target_lang' => $target_lang, 'translated_text' => $translated_text, 'hash' => $hash ), array('%s', '%s', '%s', '%s', '%s') ); } /** * 清理旧缓存 */ public function cleanup($days_old = 30) { global $wpdb; $wpdb->query($wpdb->prepare( "DELETE FROM {$this->cache_table} WHERE created_at < DATE_SUB(NOW(), INTERVAL %d DAY)", $days_old )); } } 第三章:实时聊天系统开发 3.1 WebSocket通信实现 实时聊天需要双向通信,我们可以使用WebSocket技术: class MCS_WebSocket_Server { private $server; private $clients = []; private $translator; public function __construct($translator) { $this->translator = $translator; } /** * 启动WebSocket服务器 */ public function start($host = '0.0.0.0', $port = 8080) { $this->server = new SwooleWebSocketServer($host, $port); $this->server->on('open', function($server, $request) { $this->clients[$request->fd] = [ 'fd' => $request->fd, 'user_id' => 0, 'language' => 'en', 'room_id' => null ]; echo "客户端 {$request->fd} 已连接n"; }); $this->server->on('message', function($server, $frame) { $this->handle_message($frame->fd, $frame->data); }); $this->server->on('close', function($server, $fd) { unset($this->clients[$fd]); echo "客户端 {$fd} 已断开连接n"; }); $this->server->start(); } /** * 处理客户端消息 */ private function handle_message($fd, $data) { $message = json_decode($data, true); if (!$message || !isset($message['type'])) { return; } switch ($message['type']) { case 'auth': $this->handle_auth($fd, $message); break; case 'chat_message': $this->handle_chat_message($fd, $message); break; case 'join_room': $this->handle_join_room($fd, $message); break; case 'typing': $this->handle_typing($fd, $message); break; } } /** * 处理认证 */ private function handle_auth($fd, $message) { if (isset($message['user_id'], $message['language'])) { $this->clients[$fd]['user_id'] = intval($message['user_id']); $this->clients[$fd]['language'] = sanitize_text_field($message['language']); $this->send_to_client($fd, [ 'type' => 'auth_success', 'message' => '认证成功' ]); } } /** * 处理聊天消息 */ private function handle_chat_message($fd, $message) { if (!isset($message['content'], $message['room_id'])) { return; } $client = $this->clients[$fd]; $room_id = $message['room_id']; $original_content = sanitize_text_field($message['content']); // 保存消息到数据库 $message_id = $this->save_message_to_db( $client['user_id'], $room_id, $original_content, $client['language'] ); // 向房间内所有客户端发送消息(自动翻译) $this->broadcast_to_room($room_id, [ 'type' => 'new_message', 'message_id' => $message_id, 'sender_id' => $client['user_id'], 'original_content' => $original_content, 'original_language' => $client['language'], 'timestamp' => current_time('mysql') ], $fd); } /** * 向客户端发送消息 */ private function send_to_client($fd, $data) { if ($this->server->exist($fd)) { $this->server->push($fd, json_encode($data)); } } /** * 向房间广播消息 */ private function broadcast_to_room($room_id, $data, $exclude_fd = null) { foreach ($this->clients as $client) { if ($client['room_id'] == $room_id && $client['fd'] != $exclude_fd) { // 根据客户端语言翻译消息 if (isset($data['original_content'])) { $translation = $this->translator->translate( $data['original_content'], $client['language'], $data['original_language'] ); if ($translation['success']) { $data['translated_content'] = $translation['translatedText']; } } $this->send_to_client($client['fd'], $data); } } } } 3.2 前端聊天界面 创建响应式的前端聊天界面: class MCSChatWidget { constructor(options) { this.options = Object.assign({ position: 'bottom-right', primaryColor: '#0073aa', defaultLanguage: 'en', availableLanguages: ['en', 'zh-CN', 'es', 'fr', 'de', 'ja'], autoOpen: false, greetingMessage: '您好!需要什么帮助吗?' }, options); this.ws = null; this.isConnected = false; this.currentRoom = null; this.userLanguage = this.detectUserLanguage(); this.init(); } /** * 初始化聊天组件 */ init() { this.createWidgetHTML(); this.bindEvents(); this.loadUserSettings(); if (this.options.autoOpen) { setTimeout(() => this.openChat(), 1000); } } /** * 创建聊天界面HTML */ createWidgetHTML() { // 创建主容器 this.container = document.createElement('div'); this.container.className = 'mcs-chat-container'; this.container.style.cssText = ` position: fixed; z-index: 999999; ${this.getPositionStyles()} font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; `; // 创建聊天按钮 this.chatButton = document.createElement('div'); this.chatButton.className = 'mcs-chat-button'; this.chatButton.innerHTML = ` <svg width="24" height="24" viewBox="0 0 24 24" fill="white"> <path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z"/> </svg> `; // 创建聊天窗口 this.chatWindow = document.createElement('div'); this.chatWindow.className = 'mcs-chat-window'; this.chatWindow.style.display = 'none'; this.chatWindow.innerHTML = ` <div class="mcs-chat-header"> <h3>${this.options.greetingMessage}</h3> <div class="mcs-header-actions"> <select class="mcs-language-selector"> ${this.options.availableLanguages.map(lang => `<option value="${lang}" ${lang === this.userLanguage ? 'selected' : ''}> ${this.getLanguageName(lang)} </option>` ).join('')} </select> <button class="mcs-close-chat">×</button> </div> </div> <div class="mcs-chat-messages"></div> <div class="mcs-chat-input-area"> <textarea class="mcs-message-input" placeholder="输入消息..."></textarea> <button class="mcs-send-button">发送</button> </div> `; this.container.appendChild(this.chatButton); this.container.appendChild(this.chatWindow); document.body.appendChild(this.container); this.addStyles(); } /** * 连接WebSocket服务器 */ connectWebSocket() { if (this.ws && this.isConnected) return; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.hostname}:8080`; this.ws = new WebSocket(wsUrl); this.ws.onopen = () => { this.isConnected = true; this.authenticate(); this.showNotification('已连接到聊天服务器'); }; this.ws.onmessage = (event) => { this.handleWebSocketMessage(JSON.parse(event.data)); this.ws.onclose = () => { this.isConnected = false; this.showNotification('连接已断开,正在尝试重新连接...'); setTimeout(() => this.connectWebSocket(), 3000); }; this.ws.onerror = (error) => { console.error('WebSocket错误:', error); }; } /** * 处理WebSocket消息 */ handleWebSocketMessage(data) { switch (data.type) { case 'auth_success': this.handleAuthSuccess(data); break; case 'new_message': this.handleNewMessage(data); break; case 'typing_indicator': this.handleTypingIndicator(data); break; case 'room_joined': this.handleRoomJoined(data); break; case 'error': this.showNotification(data.message, 'error'); break; } } /** * 处理新消息 */ handleNewMessage(data) { const messagesContainer = this.chatWindow.querySelector('.mcs-chat-messages'); const messageElement = this.createMessageElement(data); messagesContainer.appendChild(messageElement); messagesContainer.scrollTop = messagesContainer.scrollHeight; // 显示桌面通知(如果用户不在当前标签页) if (document.hidden && this.options.desktopNotifications) { this.showDesktopNotification(data); } } /** * 创建消息元素 */ createMessageElement(data) { const messageDiv = document.createElement('div'); messageDiv.className = `mcs-message ${data.sender_id === this.userId ? 'own-message' : 'other-message'}`; const time = new Date(data.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); let content = ` <div class="mcs-message-content"> <div class="mcs-original-text">${this.escapeHtml(data.original_content)}</div> `; if (data.translated_content && data.translated_content !== data.original_content) { content += ` <div class="mcs-translated-text"> <small>翻译:</small> ${this.escapeHtml(data.translated_content)} </div> `; } content += ` <div class="mcs-message-time">${time}</div> </div> `; messageDiv.innerHTML = content; return messageDiv; } /** * 绑定事件 */ bindEvents() { // 聊天按钮点击事件 this.chatButton.addEventListener('click', () => this.toggleChat()); // 关闭按钮事件 this.chatWindow.querySelector('.mcs-close-chat').addEventListener('click', () => this.closeChat()); // 发送按钮事件 this.chatWindow.querySelector('.mcs-send-button').addEventListener('click', () => this.sendMessage()); // 输入框回车发送 this.chatWindow.querySelector('.mcs-message-input').addEventListener('keypress', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.sendMessage(); } }); // 输入框输入事件(显示正在输入) this.chatWindow.querySelector('.mcs-message-input').addEventListener('input', (e) => { this.sendTypingIndicator(); }); // 语言选择器变更事件 this.chatWindow.querySelector('.mcs-language-selector').addEventListener('change', (e) => { this.userLanguage = e.target.value; this.saveUserSettings(); this.updateLanguage(); }); } /** * 发送消息 */ sendMessage() { const input = this.chatWindow.querySelector('.mcs-message-input'); const message = input.value.trim(); if (!message || !this.isConnected || !this.currentRoom) return; // 发送到WebSocket服务器 this.ws.send(JSON.stringify({ type: 'chat_message', room_id: this.currentRoom, content: message, language: this.userLanguage })); // 清空输入框 input.value = ''; input.focus(); } /** * 发送正在输入指示 */ sendTypingIndicator() { if (!this.isConnected || !this.currentRoom) return; this.ws.send(JSON.stringify({ type: 'typing', room_id: this.currentRoom, is_typing: true })); // 清除之前的定时器 if (this.typingTimeout) { clearTimeout(this.typingTimeout); } // 设置停止输入指示 this.typingTimeout = setTimeout(() => { if (this.isConnected) { this.ws.send(JSON.stringify({ type: 'typing', room_id: this.currentRoom, is_typing: false })); } }, 1000); } /** * 添加CSS样式 */ addStyles() { const style = document.createElement('style'); style.textContent = ` .mcs-chat-button { width: 60px; height: 60px; background-color: ${this.options.primaryColor}; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 2px 10px rgba(0,0,0,0.2); transition: all 0.3s ease; } .mcs-chat-button:hover { transform: scale(1.1); box-shadow: 0 4px 15px rgba(0,0,0,0.3); } .mcs-chat-window { width: 350px; height: 500px; background: white; border-radius: 10px; box-shadow: 0 5px 25px rgba(0,0,0,0.15); display: flex; flex-direction: column; overflow: hidden; } .mcs-chat-header { background: ${this.options.primaryColor}; color: white; padding: 15px; display: flex; justify-content: space-between; align-items: center; } .mcs-chat-header h3 { margin: 0; font-size: 16px; font-weight: 500; } .mcs-header-actions { display: flex; align-items: center; gap: 10px; } .mcs-language-selector { background: rgba(255,255,255,0.2); border: none; color: white; padding: 5px; border-radius: 4px; font-size: 12px; } .mcs-close-chat { background: none; border: none; color: white; font-size: 24px; cursor: pointer; line-height: 1; } .mcs-chat-messages { flex: 1; padding: 15px; overflow-y: auto; background: #f8f9fa; } .mcs-message { margin-bottom: 15px; max-width: 80%; } .mcs-message.own-message { margin-left: auto; } .mcs-message.other-message { margin-right: auto; } .mcs-message-content { background: white; padding: 10px 15px; border-radius: 18px; box-shadow: 0 1px 2px rgba(0,0,0,0.1); } .own-message .mcs-message-content { background: ${this.options.primaryColor}; color: white; border-bottom-right-radius: 4px; } .other-message .mcs-message-content { border-bottom-left-radius: 4px; } .mcs-translated-text { margin-top: 5px; padding-top: 5px; border-top: 1px dashed rgba(0,0,0,0.1); font-size: 0.9em; color: #666; } .own-message .mcs-translated-text { border-top-color: rgba(255,255,255,0.3); color: rgba(255,255,255,0.9); } .mcs-message-time { font-size: 11px; color: #999; margin-top: 5px; text-align: right; } .mcs-chat-input-area { border-top: 1px solid #e0e0e0; padding: 15px; background: white; } .mcs-message-input { width: 100%; border: 1px solid #ddd; border-radius: 20px; padding: 10px 15px; resize: none; font-family: inherit; font-size: 14px; min-height: 40px; max-height: 100px; box-sizing: border-box; } .mcs-message-input:focus { outline: none; border-color: ${this.options.primaryColor}; } .mcs-send-button { background: ${this.options.primaryColor}; color: white; border: none; border-radius: 20px; padding: 8px 20px; margin-top: 10px; cursor: pointer; font-weight: 500; float: right; } .mcs-send-button:hover { opacity: 0.9; } @media (max-width: 480px) { .mcs-chat-window { width: 100%; height: 100%; border-radius: 0; position: fixed; top: 0; left: 0; right: 0; bottom: 0; } } `; document.head.appendChild(style); } } ## 第四章:多语言客服工具集成 ### 4.1 客服工单系统 除了实时聊天,我们还需要一个完整的客服工单系统: class MCS_Support_Ticket_System { private $db; public function __construct() { global $wpdb; $this->db = $wpdb; } /** * 创建工单表 */ public function create_tables() { $charset_collate = $this->db->get_charset_collate(); $tickets_table = $this->db->prefix . 'mcs_support_tickets'; $tickets_sql = "CREATE TABLE IF NOT EXISTS $tickets_table ( id bigint(20) NOT NULL AUTO_INCREMENT, ticket_number varchar(50) NOT NULL, user_id bigint(20) NOT NULL, subject varchar(255) NOT NULL, description text NOT NULL, status varchar(50) DEFAULT 'open', priority varchar(50) DEFAULT 'medium', language varchar(10) NOT NULL, assigned_to bigint(20) DEFAULT 0, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY ticket_number (ticket_number), KEY user_id (user_id), KEY status (status), KEY assigned_to (assigned_to) ) $charset_collate;"; $replies_table = $this->db->prefix . 'mcs_ticket_replies'; $replies_sql = "CREATE TABLE IF NOT EXISTS $replies_table ( id bigint(20) NOT NULL AUTO_INCREMENT, ticket_id bigint(20) NOT NULL, user_id bigint(20) NOT NULL, message text NOT NULL, original_language varchar(10) NOT NULL, is_internal tinyint(1) DEFAULT 0, attachments text, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY ticket_id (ticket_id), KEY user_id (user_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($tickets_sql); dbDelta($replies_sql); } /** * 创建新工单 */ public function create_ticket($data) { $ticket_number = 'TICKET-' . date('Ymd') . '-' . strtoupper(wp_generate_password(6, false)); $result = $this->db->insert( $this->db->prefix . 'mcs_support_tickets', array( 'ticket_number' => $ticket_number, 'user_id' => $data['user_id'], 'subject' => sanitize_text_field($data['subject']), 'description' => wp_kses_post($data['description']), 'language' => sanitize_text_field($data['language']), 'priority' => $data['priority'] ?? 'medium', 'status' => 'open' ), array('%s', '%d', '%s', '%s', '%s', '%s', '%s') ); if ($result) { $ticket_id = $this->db->insert_id; // 发送通知邮件 $this->send_ticket_notification($ticket_id, 'created'); // 记录活动日志 $this->log_ticket_activity($ticket_id, 'ticket_created', $data['user_id']); return array( 'success' => true, 'ticket_id' => $ticket_id, 'ticket_number' => $ticket_number ); } return array( 'success' => false, 'error' => '创建工单失败' ); } /** * 添加工单回复 */ public function add_reply($ticket_id, $data) { $ticket = $this->get_ticket($ticket_id); if (!$ticket) { return array('success' => false, 'error' => '工单不存在'); } $result = $this->db->insert( $this->db->prefix . 'mcs_ticket_replies', array( 'ticket_id' => $ticket_id, 'user_id' => $data['user_id'], 'message' => wp_kses_post($data['message']), 'original_language' => $data['language'], 'is_internal' => $data['is_internal'] ?? 0 ), array('%d', '%d', '%s', '%s', '%d') ); if ($result) { // 更新工单状态 $this->update_ticket_status($ticket_id, $data['user_id']); // 发送通知 $this->send_ticket_notification($ticket_id, 'replied', $data['user_id']); // 自动翻译回复内容 $this->translate_ticket_reply($this->db->insert_id, $ticket['language']); return array('success' => true, 'reply_id' => $this->db->insert_id); } return array('success' => false, 'error' => '添加回复失败'); } /** * 自动翻译工单回复 */ private function translate_ticket_reply($reply_id, $target_language) { $reply = $this->db->get_row($this->db->prepare( "SELECT * FROM {$this->db->prefix}mcs_ticket_replies WHERE id = %d", $reply_id )); if ($reply && $reply->original_language !== $target_language) { $translation = $this->translate_content($reply->message, $target_language, $reply->original_language); if ($translation['success']) { // 保存翻译后的内容 update_comment_meta($reply_id, '_translated_message', $translation['translatedText']); update_comment_meta($reply_id, '_translation_language', $target_language); } } } /** * 获取工单详情 */ public function get_ticket($ticket_id) { return $this->db->get_row($this->db->prepare( "SELECT t.*, u.user_email, u.display_name FROM {$this->db->prefix}mcs_support_tickets t LEFT JOIN {$this->db->prefix}users u ON t.user_id = u.ID WHERE t.id = %d", $ticket_id )); } /** * 获取工单回复 */ public function get_ticket_replies($ticket_id, $user_language = null) { $replies = $this->db->get_results($this->db->prepare( "SELECT r.*, u.display_name, u.user_email FROM {$this->db->prefix}mcs_ticket_replies r LEFT JOIN {$this->db->prefix}users u ON r.user_id = u.ID WHERE r.ticket_id = %d ORDER BY r.created_at ASC", $ticket_id )); // 根据用户语言提供翻译版本 if ($user_language) { foreach ($replies as &$reply) { if ($reply->original_language !== $user_language) { $translated = get_comment_meta($reply->id, '_translated_message', true); if ($translated) { $reply->translated_message = $translated; } } } } return $replies; } } ### 4.2 知识库系统 创建多语言知识库,帮助用户自助解决问题: class MCS_Knowledge_Base { private $db; public function __construct()
发表评论