跳至内容

分类: 网站建设

一步步教你,在WordPress中添加网站深色模式切换与个性化主题定制器

一步步教你,在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平台上开发一个集在线简历制作与智能职位匹配功能于一体的工具。通过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插件开发的无限可能 在当今全球化的互联网环境中,多语言支持已成为网站建设的标配功能。对于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()

发表评论

手把手教程,为WordPress实现基于地理围栏的网站内容区域化展示工具

手把手教程:为WordPress实现基于地理围栏的网站内容区域化展示工具 引言:地理围栏技术与网站内容区域化的价值 在当今全球化的互联网环境中,网站访问者可能来自世界各地。然而,并非所有内容都适合向所有地区的用户展示。基于地理围栏的内容区域化技术能够根据用户的地理位置,智能展示相关性强、合规性高的内容,这已成为现代网站开发的重要趋势。 地理围栏(Geo-fencing)是一种基于位置的服务技术,通过GPS、RFID、Wi-Fi或蜂窝数据等定位方式,在现实世界中创建一个虚拟的边界。当设备进入或离开这个边界时,可以触发预设的操作或通知。在网站开发中,这一技术可以用于根据用户的地理位置展示不同的内容、广告或功能。 对于WordPress网站而言,实现基于地理围栏的内容区域化展示具有多重价值: 提升用户体验:向用户展示与其地理位置相关的内容,如本地新闻、活动、产品等 合规性管理:根据不同地区的法律法规展示合规内容 营销精准化:针对不同地区实施差异化的营销策略 内容优化:根据地区特点优化内容展示,提高转化率 本教程将详细介绍如何通过WordPress代码二次开发,实现一个基于地理围栏的网站内容区域化展示工具,让您的网站具备智能化的区域内容展示能力。 第一部分:准备工作与环境配置 1.1 理解WordPress开发基础 在开始开发之前,我们需要确保具备以下基础知识: WordPress主题和插件的基本结构 PHP编程基础 JavaScript/jQuery基础 WordPress钩子(Hooks)和过滤器(Filters)的使用 基本的HTML/CSS知识 1.2 开发环境搭建 本地开发环境:建议使用XAMPP、MAMP或Local by Flywheel等工具搭建本地WordPress开发环境 代码编辑器:推荐使用VS Code、PHPStorm或Sublime Text 版本控制:使用Git进行代码版本管理 测试工具:浏览器开发者工具、Postman等API测试工具 1.3 创建自定义插件 我们将创建一个独立的WordPress插件来实现地理围栏功能,这样可以确保功能的独立性和可移植性。 在WordPress的wp-content/plugins/目录下创建新文件夹geo-fencing-content,并在其中创建主插件文件: <?php /** * Plugin Name: 地理围栏内容区域化工具 * Plugin URI: https://yourwebsite.com/ * Description: 基于用户地理位置实现网站内容区域化展示的WordPress插件 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later * Text Domain: geo-fencing-content */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('GFC_VERSION', '1.0.0'); define('GFC_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('GFC_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 require_once GFC_PLUGIN_DIR . 'includes/class-geo-fencing-core.php'; function gfc_init() { $plugin = new Geo_Fencing_Core(); $plugin->run(); } add_action('plugins_loaded', 'gfc_init'); 第二部分:地理定位技术与实现方案 2.1 地理位置获取方案比较 实现地理围栏功能的第一步是获取用户的准确地理位置。以下是几种常用的技术方案: HTML5 Geolocation API:浏览器原生支持,精度较高,但需要用户授权 IP地址定位:通过用户IP地址推断地理位置,无需用户授权但精度有限 第三方定位服务:如MaxMind、IPinfo等专业服务,精度和可靠性较高 2.2 实现IP地址定位功能 我们将首先实现基于IP地址的地理定位功能,作为基础方案。创建includes/class-geo-location.php文件: <?php class Geo_Location { private $api_key; private $cache_time; public function __construct() { // 可以从插件设置中获取API密钥 $this->api_key = get_option('gfc_ipapi_key', ''); $this->cache_time = 24 * 60 * 60; // 缓存24小时 } /** * 获取用户IP地址 */ public function get_user_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); // 验证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']; } /** * 通过IP地址获取地理位置信息 */ public function get_location_by_ip($ip = '') { if (empty($ip)) { $ip = $this->get_user_ip(); } // 检查缓存 $cache_key = 'gfc_location_' . md5($ip); $cached_data = get_transient($cache_key); if ($cached_data !== false) { return $cached_data; } // 使用ip-api.com免费服务(限制:45次/分钟) $url = "http://ip-api.com/json/{$ip}"; if (!empty($this->api_key)) { // 如果有付费API密钥,可以使用更专业的服务 // $url = "https://api.ipgeolocation.io/ipgeo?apiKey={$this->api_key}&ip={$ip}"; } $response = wp_remote_get($url, array('timeout' => 5)); if (is_wp_error($response)) { // 如果API请求失败,使用备用方法 return $this->get_fallback_location(); } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); // 标准化返回数据 $location_data = array( 'country' => isset($data['country']) ? $data['country'] : '', 'country_code' => isset($data['countryCode']) ? $data['countryCode'] : '', 'region' => isset($data['region']) ? $data['region'] : '', 'region_name' => isset($data['regionName']) ? $data['regionName'] : '', 'city' => isset($data['city']) ? $data['city'] : '', 'zip' => isset($data['zip']) ? $data['zip'] : '', 'lat' => isset($data['lat']) ? $data['lat'] : 0, 'lon' => isset($data['lon']) ? $data['lon'] : 0, 'timezone' => isset($data['timezone']) ? $data['timezone'] : '', 'isp' => isset($data['isp']) ? $data['isp'] : '', 'ip' => $ip ); // 缓存结果 set_transient($cache_key, $location_data, $this->cache_time); return $location_data; } /** * 备用定位方法 */ private function get_fallback_location() { // 这里可以添加备用定位逻辑 // 例如使用其他免费API或默认位置 return array( 'country' => '未知', 'country_code' => 'UN', 'region' => '', 'city' => '', 'lat' => 0, 'lon' => 0, 'ip' => $this->get_user_ip() ); } /** * 计算两点之间的距离(用于地理围栏判断) */ public function calculate_distance($lat1, $lon1, $lat2, $lon2, $unit = 'km') { $theta = $lon1 - $lon2; $dist = sin(deg2rad($lat1)) * sin(deg2rad($lat2)) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * cos(deg2rad($theta)); $dist = acos($dist); $dist = rad2deg($dist); $miles = $dist * 60 * 1.1515; switch ($unit) { case 'km': return $miles * 1.609344; case 'm': return $miles * 1.609344 * 1000; case 'mile': default: return $miles; } } } 第三部分:地理围栏系统设计与实现 3.1 地理围栏数据结构设计 我们需要设计一个灵活的地理围栏系统,支持多种形状的围栏(圆形、多边形等)。首先在数据库中创建存储地理围栏的表。 创建数据库表的代码可以放在插件激活钩子中。在class-geo-fencing-core.php中添加: class Geo_Fencing_Core { public function __construct() { // 构造函数 } public function run() { // 注册激活钩子 register_activation_hook(__FILE__, array($this, 'activate_plugin')); // 注册其他钩子和过滤器 $this->register_hooks(); } public function activate_plugin() { // 创建数据库表 $this->create_database_tables(); // 设置默认选项 $this->set_default_options(); } private function create_database_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'geo_fences'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL, fence_type varchar(20) NOT NULL DEFAULT 'circle', coordinates text NOT NULL, radius float DEFAULT 0, content_rule text, priority int(11) DEFAULT 0, is_active tinyint(1) DEFAULT 1, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY is_active (is_active), KEY priority (priority) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 创建内容关联表 $relation_table = $wpdb->prefix . 'geo_fence_content'; $sql2 = "CREATE TABLE IF NOT EXISTS $relation_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, fence_id mediumint(9) NOT NULL, content_type varchar(50) NOT NULL, content_id bigint(20) NOT NULL, action_type varchar(50) NOT NULL DEFAULT 'show', conditions text, PRIMARY KEY (id), KEY fence_id (fence_id), KEY content_type (content_type, content_id) ) $charset_collate;"; dbDelta($sql2); } } 3.2 地理围栏管理界面 我们需要创建一个管理界面,让网站管理员可以添加、编辑和删除地理围栏。创建admin/class-geo-fencing-admin.php: class Geo_Fencing_Admin { private $plugin_name; private $version; public function __construct($plugin_name, $version) { $this->plugin_name = $plugin_name; $this->version = $version; add_action('admin_menu', array($this, 'add_admin_menu')); add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts')); } public function add_admin_menu() { add_menu_page( '地理围栏管理', '地理围栏', 'manage_options', 'geo-fencing', array($this, 'display_admin_page'), 'dashicons-location-alt', 30 ); add_submenu_page( 'geo-fencing', '地理围栏列表', '围栏列表', 'manage_options', 'geo-fencing', array($this, 'display_admin_page') ); add_submenu_page( 'geo-fencing', '添加新围栏', '添加围栏', 'manage_options', 'geo-fencing-add', array($this, 'display_add_fence_page') ); add_submenu_page( 'geo-fencing', '地理围栏设置', '设置', 'manage_options', 'geo-fencing-settings', array($this, 'display_settings_page') ); } public function display_admin_page() { include GFC_PLUGIN_DIR . 'admin/views/fence-list.php'; } public function display_add_fence_page() { include GFC_PLUGIN_DIR . 'admin/views/fence-edit.php'; } public function display_settings_page() { include GFC_PLUGIN_DIR . 'admin/views/settings.php'; } public function enqueue_admin_scripts($hook) { if (strpos($hook, 'geo-fencing') === false) { return; } // 加载Leaflet地图库 wp_enqueue_style('leaflet-css', 'https://unpkg.com/leaflet@1.7.1/dist/leaflet.css'); wp_enqueue_script('leaflet-js', 'https://unpkg.com/leaflet@1.7.1/dist/leaflet.js', array(), '1.7.1', true); // 加载插件自定义脚本 wp_enqueue_script( $this->plugin_name . '-admin', GFC_PLUGIN_URL . 'admin/js/admin.js', array('jquery', 'leaflet-js'), $this->version, true ); wp_enqueue_style( $this->plugin_name . '-admin', GFC_PLUGIN_URL . 'admin/css/admin.css', array(), $this->version ); // 传递数据到JavaScript wp_localize_script($this->plugin_name . '-admin', 'gfc_admin_data', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('gfc_admin_nonce'), 'default_lat' => get_option('gfc_default_lat', 39.9042), 'default_lng' => get_option('gfc_default_lng', 116.4074) )); } } 3.3 地理围栏编辑器实现 创建admin/views/fence-edit.php文件,实现围栏编辑界面: <div class="wrap"> <h1><?php echo isset($_GET['action']) && $_GET['action'] == 'edit' ? '编辑地理围栏' : '添加新地理围栏'; ?></h1> <form method="post" action="<?php echo admin_url('admin-post.php'); ?>" id="gfc-fence-form"> <input type="hidden" name="action" value="gfc_save_fence"> <?php wp_nonce_field('gfc_save_fence_action', 'gfc_fence_nonce'); ?> <?php if (isset($_GET['id'])): ?> <input type="hidden" name="fence_id" value="<?php echo intval($_GET['id']); ?>"> <?php endif; ?> <table class="form-table"> <tr> <th scope="row"><label for="fence_name">围栏名称</label></th> <td> <input type="text" name="fence_name" id="fence_name" class="regular-text" required> <p class="description">用于识别此地理围栏的名称</p> </td> </tr> <tr> <th scope="row"><label for="fence_type">围栏类型</label></th> <td> <select name="fence_type" id="fence_type"> <option value="circle">圆形围栏</option> <option value="polygon">多边形围栏</option> <option value="rectangle">矩形围栏</option> </select> </td> </tr> <tr id="circle_fields"> <th scope="row"><label for="center_point">中心点坐标</label></th> <td> <div class="coordinates-input"> <input type="text" name="center_lat" id="center_lat" placeholder="纬度" class="small-text"> <input type="text" name="center_lng" id="center_lng" placeholder="经度" class="small-text"> <button type="button" class="button" id="get_current_location">获取当前位置</button> </div> <p class="description">格式:纬度,经度(例如:39.9042,116.4074)</p> </td> </tr> <tr id="circle_radius_field"> <th scope="row"><label for="radius">半径</label></th> <td> <input type="number" name="radius" id="radius" min="0.1" step="0.1" class="small-text"> <select name="radius_unit"> <option value="km">公里</option> <option value="m">米</option> <option value="mile">英里</option> </select> </td> </tr> <tr id="polygon_fields" style="display:none;"> <tr id="polygon_fields" style="display:none;"> <th scope="row"><label for="polygon_coords">多边形坐标</label></th> <td> <textarea name="polygon_coords" id="polygon_coords" rows="5" cols="50" placeholder="格式:纬度,经度&#10;例如:&#10;39.9042,116.4074&#10;39.9142,116.4174&#10;39.8942,116.4274"></textarea> <p class="description">每行输入一个坐标点,格式:纬度,经度</p> </td> </tr> <tr> <th scope="row"><label for="fence_priority">优先级</label></th> <td> <input type="number" name="fence_priority" id="fence_priority" value="0" min="0" class="small-text"> <p class="description">数值越大优先级越高,当用户位于多个围栏重叠区域时生效</p> </td> </tr> <tr> <th scope="row"><label for="is_active">状态</label></th> <td> <label> <input type="checkbox" name="is_active" id="is_active" value="1" checked> 启用此围栏 </label> </td> </tr> </table> <div class="map-container"> <h3>地图预览</h3> <div id="fence_map" style="height: 400px; width: 100%;"></div> <p class="description">在地图上点击可以设置围栏中心点或添加多边形顶点</p> </div> <h2>内容规则设置</h2> <div id="content_rules"> <div class="content-rule"> <h4>规则 #1</h4> <table class="form-table"> <tr> <th scope="row"><label>内容类型</label></th> <td> <select name="content_type[]" class="content-type-select"> <option value="post">文章</option> <option value="page">页面</option> <option value="category">分类目录</option> <option value="custom">自定义内容</option> </select> </td> </tr> <tr> <th scope="row"><label>内容选择</label></th> <td> <select name="content_id[]" class="content-id-select" style="width: 300px;"> <option value="">请选择内容</option> </select> </td> </tr> <tr> <th scope="row"><label>操作类型</label></th> <td> <select name="action_type[]"> <option value="show">显示内容</option> <option value="hide">隐藏内容</option> <option value="replace">替换内容</option> <option value="redirect">重定向</option> </select> </td> </tr> <tr class="replace-content" style="display:none;"> <th scope="row"><label>替换内容</label></th> <td> <textarea name="replace_content[]" rows="3" cols="50" placeholder="输入替换内容或短代码"></textarea> </td> </tr> <tr class="redirect-url" style="display:none;"> <th scope="row"><label>重定向URL</label></th> <td> <input type="url" name="redirect_url[]" class="regular-text" placeholder="https://"> </td> </tr> </table> <hr> </div> </div> <button type="button" class="button" id="add_content_rule">添加新规则</button> <p class="submit"> <input type="submit" name="submit" id="submit" class="button button-primary" value="保存围栏"> </p> </form> </div> <script>jQuery(document).ready(function($) { // 初始化地图 var map = L.map('fence_map').setView([39.9042, 116.4074], 10); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' }).addTo(map); // 地图点击事件 var marker; var polygonPoints = []; var polygonLayer; map.on('click', function(e) { var lat = e.latlng.lat; var lng = e.latlng.lng; $('#center_lat').val(lat.toFixed(6)); $('#center_lng').val(lng.toFixed(6)); // 更新地图标记 if (marker) { map.removeLayer(marker); } marker = L.marker([lat, lng]).addTo(map); // 如果是多边形模式,添加顶点 if ($('#fence_type').val() === 'polygon') { polygonPoints.push([lat, lng]); updatePolygonPreview(); } }); // 围栏类型切换 $('#fence_type').on('change', function() { var type = $(this).val(); if (type === 'circle') { $('#circle_fields, #circle_radius_field').show(); $('#polygon_fields').hide(); } else if (type === 'polygon') { $('#circle_fields, #circle_radius_field').hide(); $('#polygon_fields').show(); polygonPoints = []; if (polygonLayer) { map.removeLayer(polygonLayer); } } }); // 更新多边形预览 function updatePolygonPreview() { if (polygonLayer) { map.removeLayer(polygonLayer); } if (polygonPoints.length >= 3) { polygonLayer = L.polygon(polygonPoints, {color: 'blue'}).addTo(map); map.fitBounds(polygonLayer.getBounds()); // 更新坐标文本 var coordsText = polygonPoints.map(function(point) { return point[0].toFixed(6) + ',' + point[1].toFixed(6); }).join('n'); $('#polygon_coords').val(coordsText); } } // 获取当前位置 $('#get_current_location').on('click', function() { if (navigator.geolocation) { navigator.geolocation.getCurrentPosition(function(position) { var lat = position.coords.latitude; var lng = position.coords.longitude; $('#center_lat').val(lat.toFixed(6)); $('#center_lng').val(lng.toFixed(6)); // 更新地图 if (marker) { map.removeLayer(marker); } marker = L.marker([lat, lng]).addTo(map); map.setView([lat, lng], 13); }); } else { alert('您的浏览器不支持地理位置功能'); } }); // 操作类型切换 $(document).on('change', 'select[name="action_type[]"]', function() { var actionType = $(this).val(); var ruleDiv = $(this).closest('.content-rule'); ruleDiv.find('.replace-content, .redirect-url').hide(); if (actionType === 'replace') { ruleDiv.find('.replace-content').show(); } else if (actionType === 'redirect') { ruleDiv.find('.redirect-url').show(); } }); // 添加新规则 $('#add_content_rule').on('click', function() { var ruleCount = $('#content_rules .content-rule').length + 1; var newRule = $('#content_rules .content-rule:first').clone(); newRule.find('h4').text('规则 #' + ruleCount); newRule.find('input, textarea, select').val(''); newRule.find('.replace-content, .redirect-url').hide(); $('#content_rules').append(newRule); }); });</script> ## 第四部分:核心功能实现与集成 ### 4.1 地理围栏检测算法 创建`includes/class-geo-fence-detector.php`,实现围栏检测逻辑: <?phpclass Geo_Fence_Detector { private $location_service; public function __construct($location_service) { $this->location_service = $location_service; } /** * 检测用户是否在指定围栏内 */ public function is_user_in_fence($fence_data, $user_location = null) { if (empty($user_location)) { $user_location = $this->location_service->get_location_by_ip(); } if (empty($user_location['lat']) || empty($user_location['lon'])) { return false; } $user_lat = floatval($user_location['lat']); $user_lng = floatval($user_location['lon']); switch ($fence_data['fence_type']) { case 'circle': return $this->check_circle_fence($fence_data, $user_lat, $user_lng); case 'polygon': return $this->check_polygon_fence($fence_data, $user_lat, $user_lng); case 'rectangle': return $this->check_rectangle_fence($fence_data, $user_lat, $user_lng); default: return false; } } /** * 检测圆形围栏 */ private function check_circle_fence($fence_data, $user_lat, $user_lng) { $center_coords = explode(',', $fence_data['coordinates']); if (count($center_coords) !== 2) { return false; } $center_lat = floatval(trim($center_coords[0])); $center_lng = floatval(trim($center_coords[1])); $radius = floatval($fence_data['radius']); // 计算距离 $distance = $this->location_service->calculate_distance( $center_lat, $center_lng, $user_lat, $user_lng, 'km' ); return $distance <= $radius; } /** * 检测多边形围栏(使用射线法) */ private function check_polygon_fence($fence_data, $user_lat, $user_lng) { $coordinates = $this->parse_polygon_coordinates($fence_data['coordinates']); if (count($coordinates) < 3) { return false; } $inside = false; $n = count($coordinates); for ($i = 0, $j = $n - 1; $i < $n; $j = $i++) { $xi = $coordinates[$i][0]; $yi = $coordinates[$i][1]; $xj = $coordinates[$j][0]; $yj = $coordinates[$j][1]; $intersect = (($yi > $user_lng) != ($yj > $user_lng)) && ($user_lat < ($xj - $xi) * ($user_lng - $yi) / ($yj - $yi) + $xi); if ($intersect) { $inside = !$inside; } } return $inside; } /** * 解析多边形坐标 */ private function parse_polygon_coordinates($coords_string) { $coordinates = array(); $lines = explode("n", $coords_string); foreach ($lines as $line) { $line = trim($line); if (empty($line)) continue; $parts = explode(',', $line); if (count($parts) === 2) { $lat = floatval(trim($parts[0])); $lng = floatval(trim($parts[1])); $coordinates[] = array($lat, $lng); } } return $coordinates; } /** * 检测矩形围栏 */ private function check_rectangle_fence($fence_data, $user_lat, $user_lng) { $bounds = explode('|', $fence_data['coordinates']); if (count($bounds) !== 4) { return false; } $min_lat = floatval($bounds[0]); $max_lat = floatval($bounds[1]); $min_lng = floatval($bounds[2]); $max_lng = floatval($bounds[3]); return ($user_lat >= $min_lat && $user_lat <= $max_lat && $user_lng >= $min_lng && $user_lng <= $max_lng); } /** * 获取用户所在的所有围栏 */ public function get_user_fences($user_location = null) { global $wpdb; $table_name = $wpdb->prefix . 'geo_fences'; // 获取所有启用的围栏 $fences = $wpdb->get_results( "SELECT * FROM $table_name WHERE is_active = 1 ORDER BY priority DESC", ARRAY_A ); $user_fences = array(); foreach ($fences as $fence) { if ($this->is_user_in_fence($fence, $user_location)) { $user_fences[] = $fence; } } return $user_fences; } /** * 获取适用于用户的内容规则 */ public function get_content_rules_for_user() { $user_fences = $this->get_user_fences(); $all_rules = array(); foreach ($user_fences as $fence) { $fence_rules = $this->get_fence_content_rules($fence['id']); $all_rules = array_merge($all_rules, $fence_rules); } // 按优先级排序 usort($all_rules, function($a, $b) { return $b['priority'] - $a['priority']; }); return $all_rules; } /** * 获取围栏的内容规则 */ private function get_fence_content_rules($fence_id) { global $wpdb; $table_name = $wpdb->prefix . 'geo_fence_content'; return $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $table_name WHERE fence_id = %d", $fence_id ), ARRAY_A ); } } ### 4.2 WordPress内容过滤与替换 创建`includes/class-content-filter.php`,实现内容过滤功能: <?phpclass Geo_Content_Filter { private $fence_detector; private $user_rules; public function __construct($fence_detector) { $this->fence_detector = $fence_detector; $this->user_rules = null; } public function init() { // 只在非管理页面应用规则 if (!is_admin()) { // 文章内容过滤 add_filter('the_content', array($this, 'filter_post_content'), 10, 1); // 小工具过滤 add_filter('widget_display_callback', array($this, 'filter_widget'), 10, 3); // 菜单过滤 add_filter('wp_nav_menu_objects', array($this, 'filter_menu_items'), 10, 2); // 重定向处理 add_action('template_redirect', array($this, 'handle_redirects')); // 短代码支持 add_shortcode('geo_content', array($this, 'geo_content_shortcode')); } } /** * 获取用户规则(懒加载) */ private function get_user_rules() { if ($this->user_rules === null) { $this->user_rules = $this->fence_detector->get_content_rules_for_user(); } return $this->user_rules; } /** * 过滤文章内容 */ public function filter_post_content($content) { global $post; if (empty($post)) { return $content; } $rules = $this->get_user_rules(); foreach ($rules as $rule) { if ($this->rule_applies_to_content($rule, $post)) { $content = $this->apply_rule_to_content($rule, $content, $post); } } return $content; } /** * 检查规则是否适用于当前内容 */ private function rule_applies_to_content($rule, $post) { switch ($rule['content_type']) { case 'post': return ($post->post_type == 'post' && $rule['content_id'] == $post->ID); case 'page': return ($post->post_type == 'page' && $rule['content_id'] == $post->ID); case 'category': return has_category($rule['content_id'], $post); case 'custom': // 自定义规则,可以扩展 return $this->check_custom_rule($rule, $post); default: return false; } } /** * 应用规则到内容 */ private function apply_rule_to_content($rule, $content, $post) { switch ($rule['action_type']) { case 'hide': return ''; // 完全隐藏内容 case 'replace': return $this->get_replacement_content($rule); case 'show': // 默认就是

发表评论

开发指南,打造网站内嵌的在线音频剪辑与播客节目制作工具

开发指南:打造网站内嵌的在线音频剪辑与播客节目制作工具 摘要 随着播客和音频内容的兴起,越来越多的网站需要集成音频处理功能。本文将详细介绍如何通过WordPress程序的代码二次开发,实现一个功能完整的在线音频剪辑与播客节目制作工具。我们将从需求分析、技术选型、架构设计到具体实现,全面解析这一过程,帮助开发者打造一个既实用又易于集成的音频处理解决方案。 一、项目背景与需求分析 1.1 音频内容的市场趋势 近年来,音频内容市场呈现爆发式增长。根据最新数据,全球播客听众数量已超过4亿,预计到2025年将增长至5.5亿。与此同时,音频内容创作者数量也在快速增长,从专业媒体机构到个人创作者,都在寻求简单易用的音频制作工具。 1.2 网站集成音频工具的需求 传统音频处理软件如Audacity、Adobe Audition等虽然功能强大,但存在以下问题: 需要下载安装,使用门槛较高 无法与网站内容管理系统无缝集成 协作功能有限,不适合团队远程工作 因此,开发一个能够内嵌在网站中的在线音频剪辑工具,具有以下优势: 降低使用门槛:用户无需安装任何软件,打开网页即可使用 无缝集成:与网站用户系统、内容管理系统深度整合 便于协作:支持多人同时编辑,实时保存进度 内容生态闭环:剪辑完成后可直接发布到网站播客频道 1.3 功能需求清单 基于用户调研,我们确定了以下核心功能需求: 基础音频处理功能: 音频文件上传与导入 多轨道时间线编辑 基本的剪切、复制、粘贴、删除操作 音量调整与淡入淡出效果 音频片段拖拽排序 高级音频处理功能: 降噪与音频增强 均衡器调整 多音频轨道混合 音频速度调整 音高修正 播客制作专用功能: 片头片尾模板 广告位标记与插入 多主持人音轨管理 实时语音转文字(字幕生成) 章节标记与时间戳生成 输出与发布功能: 多种格式导出(MP3、WAV、M4A等) 直接发布到WordPress媒体库 生成播客RSS feed 社交媒体一键分享 二、技术架构与选型 2.1 WordPress作为开发平台的优势 选择WordPress作为开发平台有以下优势: 庞大的用户基础:全球超过40%的网站使用WordPress 成熟的插件体系:便于功能扩展和模块化开发 丰富的API接口:REST API和众多钩子函数便于二次开发 强大的媒体管理:内置媒体库便于音频文件管理 用户权限系统:成熟的角色和权限管理机制 2.2 前端技术选型 音频处理核心库: Web Audio API:现代浏览器原生支持的音频处理API,性能优秀 wavesurfer.js:专业的音频波形可视化库,支持多种交互 Recorder.js:用于音频录制的轻量级库 前端框架: React:组件化开发,适合复杂交互界面 Redux:状态管理,确保复杂应用的数据一致性 UI组件库: Material-UI:提供现代化、响应式的UI组件 自定义组件:针对音频编辑器的专用UI组件 2.3 后端技术方案 WordPress核心扩展: 自定义Post Type:用于管理音频项目和播客节目 自定义字段:存储音频项目的元数据 REST API端点:提供前端所需的数据接口 AJAX处理:处理音频文件上传和实时操作 音频处理服务端组件: FFmpeg:通过PHP执行命令行处理音频文件 LAME MP3编码器:用于高质量MP3编码 音频处理队列:使用WordPress Cron或外部队列处理耗时任务 存储方案: WordPress媒体库:存储原始音频和最终成品 临时文件系统:处理过程中的临时文件存储 数据库:存储项目结构、用户操作记录等 2.4 系统架构设计 用户界面层 (React + Web Audio API) ↓ API接口层 (WordPress REST API + 自定义端点) ↓ 业务逻辑层 (自定义插件 + 音频处理服务) ↓ 数据存储层 (WordPress数据库 + 文件系统) ↓ 第三方服务层 (转码服务、语音识别API等) 三、WordPress插件开发基础 3.1 创建插件基本结构 首先,我们需要创建一个标准的WordPress插件: /* Plugin Name: 在线音频剪辑与播客制作工具 Plugin URI: https://yourwebsite.com/audio-editor Description: 网站内嵌的在线音频剪辑与播客节目制作工具 Version: 1.0.0 Author: 您的名称 License: GPL v2 or later */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('AUDIO_EDITOR_VERSION', '1.0.0'); define('AUDIO_EDITOR_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('AUDIO_EDITOR_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 require_once AUDIO_EDITOR_PLUGIN_DIR . 'includes/class-audio-editor.php'; function audio_editor_init() { $plugin = new Audio_Editor(); $plugin->run(); } add_action('plugins_loaded', 'audio_editor_init'); 3.2 创建自定义Post Type 为了管理音频项目,我们需要创建自定义Post Type: class Audio_Editor { public function __construct() { // 构造函数 } public function run() { // 注册钩子和过滤器 add_action('init', array($this, 'register_audio_project_cpt')); add_action('admin_menu', array($this, 'add_admin_menu')); add_action('wp_enqueue_scripts', array($this, 'enqueue_public_scripts')); add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts')); } // 注册音频项目自定义文章类型 public function register_audio_project_cpt() { $labels = array( 'name' => '音频项目', 'singular_name' => '音频项目', 'menu_name' => '音频编辑器', 'add_new' => '新建项目', 'add_new_item' => '新建音频项目', 'edit_item' => '编辑音频项目', 'new_item' => '新音频项目', 'view_item' => '查看项目', 'search_items' => '搜索音频项目', 'not_found' => '未找到音频项目', 'not_found_in_trash' => '回收站中无音频项目' ); $args = array( 'labels' => $labels, 'public' => true, 'publicly_queryable' => true, 'show_ui' => true, 'show_in_menu' => true, 'query_var' => true, 'rewrite' => array('slug' => 'audio-project'), 'capability_type' => 'post', 'has_archive' => true, 'hierarchical' => false, 'menu_position' => 20, 'menu_icon' => 'dashicons-format-audio', 'supports' => array('title', 'editor', 'author', 'thumbnail'), 'show_in_rest' => true, // 启用REST API支持 ); register_post_type('audio_project', $args); } } 3.3 创建自定义字段 使用WordPress的REST API和register_meta函数为音频项目添加自定义字段: // 注册音频项目的元数据字段 public function register_audio_project_meta() { $meta_fields = array( 'audio_project_data' => array( 'type' => 'string', 'description' => '音频项目数据(JSON格式)', 'single' => true, 'show_in_rest' => true, ), 'audio_duration' => array( 'type' => 'number', 'description' => '音频时长(秒)', 'single' => true, 'show_in_rest' => true, ), 'audio_format' => array( 'type' => 'string', 'description' => '音频格式', 'single' => true, 'show_in_rest' => true, ), 'project_status' => array( 'type' => 'string', 'description' => '项目状态', 'single' => true, 'show_in_rest' => true, ), ); foreach ($meta_fields as $key => $args) { register_post_meta('audio_project', $key, $args); } } add_action('init', array($this, 'register_audio_project_meta')); 四、前端音频编辑器实现 4.1 编辑器界面架构 创建React组件结构: // 主编辑器组件 import React, { useState, useEffect, useRef } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import WaveSurfer from 'wavesurfer.js'; import Timeline from './components/Timeline'; import Toolbar from './components/Toolbar'; import TrackList from './components/TrackList'; import EffectsPanel from './components/EffectsPanel'; import ExportPanel from './components/ExportPanel'; const AudioEditor = ({ projectId }) => { const [audioContext, setAudioContext] = useState(null); const [wavesurfer, setWavesurfer] = useState(null); const waveformRef = useRef(null); // 初始化音频上下文 useEffect(() => { const AudioContext = window.AudioContext || window.webkitAudioContext; const context = new AudioContext(); setAudioContext(context); // 初始化波形图 if (waveformRef.current) { const ws = WaveSurfer.create({ container: waveformRef.current, waveColor: '#4F46E5', progressColor: '#7C3AED', cursorColor: '#000', barWidth: 2, barRadius: 3, cursorWidth: 1, height: 200, barGap: 3, responsive: true, backend: 'WebAudio', }); setWavesurfer(ws); // 加载项目音频 if (projectId) { loadProjectAudio(projectId, ws); } } return () => { if (wavesurfer) { wavesurfer.destroy(); } if (audioContext) { audioContext.close(); } }; }, [projectId]); return ( <div className="audio-editor-container"> <div className="editor-header"> <h1>在线音频编辑器</h1> <Toolbar wavesurfer={wavesurfer} /> </div> <div className="editor-main"> <div className="waveform-container"> <div ref={waveformRef} id="waveform"></div> <Timeline wavesurfer={wavesurfer} /> </div> <div className="editor-sidebar"> <TrackList audioContext={audioContext} /> <EffectsPanel /> <ExportPanel projectId={projectId} /> </div> </div> </div> ); }; export default AudioEditor; 4.2 音频时间线组件 时间线是音频编辑器的核心组件,需要实现以下功能: // 时间线组件 import React, { useEffect, useRef } from 'react'; const Timeline = ({ wavesurfer }) => { const timelineRef = useRef(null); useEffect(() => { if (wavesurfer && timelineRef.current) { // 创建时间线 const TimelinePlugin = window.WaveSurfer.timeline; wavesurfer.addPlugin(TimelinePlugin.create({ container: timelineRef.current, primaryLabelInterval: 60, secondaryLabelInterval: 10, primaryColor: '#4B5563', secondaryColor: '#9CA3AF', primaryFontColor: '#6B7280', secondaryFontColor: '#9CA3AF', })).initPlugin('timeline'); } }, [wavesurfer]); return ( <div className="timeline-container"> <div ref={timelineRef} id="timeline"></div> <div className="time-display"> <span id="current-time">0:00</span> / <span id="total-time">0:00</span> </div> </div> ); }; export default Timeline; 4.3 音频轨道管理系统 实现多轨道音频管理: // 轨道列表组件 import React, { useState } from 'react'; import TrackItem from './TrackItem'; const TrackList = ({ audioContext }) => { const [tracks, setTracks] = useState([]); const [nextTrackId, setNextTrackId] = useState(1); // 添加新轨道 const addTrack = () => { const newTrack = { id: nextTrackId, name: `轨道 ${nextTrackId}`, volume: 1.0, pan: 0, muted: false, solo: false, clips: [], audioBuffer: null, sourceNode: null, gainNode: null, pannerNode: null, }; // 创建音频节点 if (audioContext) { newTrack.gainNode = audioContext.createGain(); newTrack.pannerNode = audioContext.createStereoPanner(); // 连接节点 newTrack.gainNode.connect(newTrack.pannerNode); newTrack.pannerNode.connect(audioContext.destination); } setTracks([...tracks, newTrack]); setNextTrackId(nextTrackId + 1); }; // 删除轨道 const removeTrack = (trackId) => { setTracks(tracks.filter(track => track.id !== trackId)); }; // 更新轨道属性 const updateTrack = (trackId, updates) => { setTracks(tracks.map(track => track.id === trackId ? { ...track, ...updates } : track )); }; return ( <div className="track-list"> <div className="track-list-header"> <h3>音频轨道</h3> <button onClick={addTrack} className="add-track-btn"> + 添加轨道 </button> </div> <div className="tracks"> {tracks.map(track => ( <TrackItem key={track.id} track={track} onUpdate={updateTrack} onRemove={removeTrack} audioContext={audioContext} /> ))} </div> </div> ); }; export default TrackList; 五、音频处理功能实现 5.1 音频文件上传与处理 在WordPress中处理音频文件上传: // 处理音频文件上传 public function handle_audio_upload() { // 验证nonce if (!wp_verify_nonce($_POST['nonce'], 'audio_editor_nonce')) { wp_die('安全验证失败'); } // 检查用户权限 if (!current_user_can('upload_files')) { wp_die('权限不足'); } // 处理文件上传 $file = $_FILES['audio_file']; // 检查文件类型 $allowed_types = array('audio/mpeg', 'audio/wav', 'audio/x-wav', 'audio/mp4'); if (!in_array($file['type'], $allowed_types)) { wp_send_json_error('不支持的文件格式'); } // 上传文件到WordPress媒体库 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'])) { wp_send_json_error($uploaded_file['error']); } // 创建媒体附件 $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); // 获取音频信息 $audio_info = $this->get_audio_info($uploaded_file['file']); // 返回响应 wp_send_json_success(array( 'id' => $attach_id, 'url' => $uploaded_file['url'], 'title' => $attachment['post_title'], 'duration' => $audio_info['duration'], 'format' => $audio_info['format'], )); } // 获取音频文件信息 private function get_audio_info($file_path) { $info = array( 'duration' => 0, 'format' => '', 'bitrate' => 0, 'sample_rate' => 0, ); // 使用FFmpeg获取音频信息 // 检查FFmpeg是否可用 if (function_exists('shell_exec')) { $ffmpeg_path = $this->get_ffmpeg_path(); if ($ffmpeg_path) { $command = escapeshellcmd($ffmpeg_path) . " -i " . escapeshellarg($file_path) . " 2>&1"; $output = shell_exec($command); // 解析FFmpeg输出获取音频信息 if (preg_match('/Duration: (d{2}):(d{2}):(d{2}.d{2})/', $output, $matches)) { $hours = intval($matches[1]); $minutes = intval($matches[2]); $seconds = floatval($matches[3]); $info['duration'] = $hours * 3600 + $minutes * 60 + $seconds; } if (preg_match('/Audio: (w+)/', $output, $matches)) { $info['format'] = $matches[1]; } if (preg_match('/bitrate: (d+) kb/s/', $output, $matches)) { $info['bitrate'] = intval($matches[1]); } if (preg_match('/(d+) Hz/', $output, $matches)) { $info['sample_rate'] = intval($matches[1]); } } } return $info; } // 获取FFmpeg路径 private function get_ffmpeg_path() { // 尝试常见路径 $possible_paths = array( '/usr/bin/ffmpeg', '/usr/local/bin/ffmpeg', '/opt/homebrew/bin/ffmpeg', 'ffmpeg', // 如果已在PATH中 ); foreach ($possible_paths as $path) { if (is_executable($path)) { return $path; } } // 检查shell命令是否可用 $test = shell_exec('which ffmpeg 2>/dev/null'); if ($test) { return trim($test); } return false; } 5.2 音频剪辑核心功能实现 在前端实现音频剪辑的核心功能: // 音频剪辑管理器 class AudioClipManager { constructor(audioContext) { this.audioContext = audioContext; this.clips = []; this.isPlaying = false; this.startTime = 0; this.currentTime = 0; this.playbackRate = 1.0; } // 添加音频片段 async addClip(trackId, audioBuffer, startTime, duration, offset = 0) { const clip = { id: Date.now() + Math.random(), trackId, audioBuffer, startTime, // 在时间轴上的开始时间 duration, offset, // 在源音频中的偏移量 sourceNode: null, gainNode: null, isMuted: false, fadeIn: { duration: 0, type: 'linear' }, fadeOut: { duration: 0, type: 'linear' }, effects: [] }; this.clips.push(clip); return clip; } // 剪切音频片段 splitClip(clipId, splitTime) { const clipIndex = this.clips.findIndex(c => c.id === clipId); if (clipIndex === -1) return null; const originalClip = this.clips[clipIndex]; const splitPosition = splitTime - originalClip.startTime; if (splitPosition <= 0 || splitPosition >= originalClip.duration) { return null; } // 创建第一个片段 const firstClip = { ...originalClip, id: Date.now() + Math.random(), duration: splitPosition }; // 创建第二个片段 const secondClip = { ...originalClip, id: Date.now() + Math.random() + 1, startTime: splitTime, offset: originalClip.offset + splitPosition, duration: originalClip.duration - splitPosition }; // 替换原片段 this.clips.splice(clipIndex, 1, firstClip, secondClip); return [firstClip, secondClip]; } // 合并音频片段 mergeClips(clipIds) { const clipsToMerge = this.clips.filter(c => clipIds.includes(c.id)); if (clipsToMerge.length < 2) return null; // 按开始时间排序 clipsToMerge.sort((a, b) => a.startTime - b.startTime); // 检查片段是否连续 for (let i = 1; i < clipsToMerge.length; i++) { const prevClip = clipsToMerge[i - 1]; const currentClip = clipsToMerge[i]; if (prevClip.startTime + prevClip.duration !== currentClip.startTime) { console.error('片段不连续,无法合并'); return null; } } // 创建合并后的片段 const mergedClip = { ...clipsToMerge[0], id: Date.now() + Math.random(), duration: clipsToMerge.reduce((sum, clip) => sum + clip.duration, 0) }; // 移除原片段,添加新片段 this.clips = this.clips.filter(c => !clipIds.includes(c.id)); this.clips.push(mergedClip); return mergedClip; } // 播放音频 play() { if (this.isPlaying) return; this.isPlaying = true; this.startTime = this.audioContext.currentTime - this.currentTime; this.scheduleClips(); } // 暂停音频 pause() { if (!this.isPlaying) return; this.isPlaying = false; this.currentTime = this.audioContext.currentTime - this.startTime; // 停止所有音频源 this.clips.forEach(clip => { if (clip.sourceNode) { clip.sourceNode.stop(); clip.sourceNode = null; } }); } // 调度音频片段播放 scheduleClips() { const currentTime = this.audioContext.currentTime; const playbackStartTime = this.startTime + this.currentTime; this.clips.forEach(clip => { if (clip.isMuted) return; const clipStartTime = clip.startTime / this.playbackRate; const clipEndTime = clipStartTime + clip.duration / this.playbackRate; // 如果片段在当前播放位置之后,调度播放 if (clipEndTime > this.currentTime && clipStartTime < this.currentTime + 10) { this.scheduleClipPlayback(clip, playbackStartTime); } }); } // 调度单个片段播放 scheduleClipPlayback(clip, playbackStartTime) { const sourceNode = this.audioContext.createBufferSource(); const gainNode = this.audioContext.createGain(); sourceNode.buffer = clip.audioBuffer; sourceNode.playbackRate.value = this.playbackRate; // 设置增益节点(音量控制) gainNode.gain.setValueAtTime(0, playbackStartTime + clip.startTime / this.playbackRate); // 淡入效果 if (clip.fadeIn.duration > 0) { gainNode.gain.linearRampToValueAtTime( 1, playbackStartTime + clip.startTime / this.playbackRate + clip.fadeIn.duration ); } else { gainNode.gain.setValueAtTime(1, playbackStartTime + clip.startTime / this.playbackRate); } // 淡出效果 if (clip.fadeOut.duration > 0) { const fadeOutStart = playbackStartTime + (clip.startTime + clip.duration - clip.fadeOut.duration) / this.playbackRate; gainNode.gain.setValueAtTime(1, fadeOutStart); gainNode.gain.linearRampToValueAtTime( 0, fadeOutStart + clip.fadeOut.duration / this.playbackRate ); } // 连接节点 sourceNode.connect(gainNode); gainNode.connect(this.audioContext.destination); // 应用效果器 clip.effects.forEach(effect => { this.applyEffect(effect, gainNode); }); // 开始播放 const startTime = Math.max( 0, playbackStartTime + clip.startTime / this.playbackRate - this.currentTime ); sourceNode.start( this.audioContext.currentTime + startTime, clip.offset / this.playbackRate, clip.duration / this.playbackRate ); // 保存节点引用 clip.sourceNode = sourceNode; clip.gainNode = gainNode; } // 应用音频效果 applyEffect(effect, inputNode) { switch (effect.type) { case 'equalizer': const eq = this.audioContext.createBiquadFilter(); eq.type = effect.filterType || 'peaking'; eq.frequency.value = effect.frequency || 1000; eq.gain.value = effect.gain || 0; eq.Q.value = effect.Q || 1; inputNode.disconnect(); inputNode.connect(eq); eq.connect(this.audioContext.destination); break; case 'compressor': const compressor = this.audioContext.createDynamicsCompressor(); compressor.threshold.value = effect.threshold || -24; compressor.knee.value = effect.knee || 30; compressor.ratio.value = effect.ratio || 12; compressor.attack.value = effect.attack || 0.003; compressor.release.value = effect.release || 0.25; inputNode.disconnect(); inputNode.connect(compressor); compressor.connect(this.audioContext.destination); break; case 'reverb': const convolver = this.audioContext.createConvolver(); // 这里需要加载脉冲响应文件 // 简化实现:创建人工混响 const reverbGain = this.audioContext.createGain(); reverbGain.gain.value = effect.mix || 0.5; inputNode.disconnect(); inputNode.connect(this.audioContext.destination); // 干声 inputNode.connect(reverbGain); reverbGain.connect(convolver); convolver.connect(this.audioContext.destination); // 湿声 break; } } } 5.3 音频效果处理器 实现常用的音频效果处理: // 音频效果处理器 class AudioEffectProcessor { constructor(audioContext) { this.audioContext = audioContext; this.effects = new Map(); } // 创建均衡器效果 createEqualizer(params = {}) { const eq = { type: 'equalizer', bands: [ { frequency: 60, gain: 0, type: 'lowshelf' }, { frequency: 230, gain: 0, type: 'peaking' }, { frequency: 910, gain: 0, type: 'peaking' }, { frequency: 4000, gain: 0, type: 'peaking' }, { frequency: 14000, gain: 0, type: 'highshelf' } ], ...params }; const effectId = 'eq_' + Date.now(); this.effects.set(effectId, eq); return { id: effectId, apply: (inputNode) => this.applyEqualizer(inputNode, eq), update: (updates) => this.updateEffect(effectId, updates) }; } // 应用均衡器 applyEqualizer(inputNode, eq) { const nodes = []; let lastNode = inputNode; eq.bands.forEach((band, index) => { const filter = this.audioContext.createBiquadFilter(); filter.type = band.type; filter.frequency.value = band.frequency; filter.gain.value = band.gain; filter.Q.value = band.Q || 1; lastNode.disconnect(); lastNode.connect(filter); lastNode = filter; nodes.push(filter); }); return { input: inputNode, output: lastNode, nodes: nodes, updateBand: (bandIndex, updates) => { if (nodes[bandIndex]) { Object.keys(updates).forEach(key => { if (nodes[bandIndex][key] && typeof nodes[bandIndex][key].setValueAtTime === 'function') { nodes[bandIndex][key].setValueAtTime(updates[key], this.audioContext.currentTime); } }); } } }; } // 创建压缩器效果 createCompressor(params = {}) { const compressor = this.audioContext.createDynamicsCompressor(); // 设置参数 compressor.threshold.value = params.threshold || -24; compressor.knee.value = params.knee || 30; compressor.ratio.value = params.ratio || 12; compressor.attack.value = params.attack || 0.003; compressor.release.value = params.release || 0.25; const effectId = 'comp_' + Date.now(); this.effects.set(effectId, { type: 'compressor', node: compressor, params: params }); return { id: effectId, apply: (inputNode) => { inputNode.disconnect(); inputNode.connect(compressor); return { input: inputNode, output: compressor, update: (updates) => this.updateCompressor(compressor, updates) }; } }; } // 更新压缩器参数 updateCompressor(compressor, updates) { Object.keys(updates).forEach(param => { if (compressor[param] && typeof compressor[param].setValueAtTime === 'function') { compressor[param].setValueAtTime(updates[param], this.audioContext.currentTime); } }); } // 创建噪声消除效果 async createNoiseReduction(noiseProfile) { // 注意:完整的噪声消除需要复杂的信号处理 // 这里提供简化实现 const effectId = 'noise_' + Date.now(); // 创建高通滤波器去除低频噪声 const highpass = this.audioContext.createBiquadFilter(); highpass.type = 'highpass'; highpass.frequency.value = 80; // 去除80Hz以下的噪声 // 创建噪声门 const noiseGate = this.audioContext.createGain(); noiseGate.gain.value = 1; // 简单的噪声门实现(需要更复杂的实现用于实际应用) const analyser = this.audioContext.createAnalyser(); analyser.fftSize = 2048; this.effects.set(effectId, { type: 'noiseReduction', nodes: { highpass, noiseGate, analyser } }); return { id: effectId, apply: (inputNode) => { inputNode.disconnect(); inputNode.connect(highpass); highpass.connect(noiseGate); noiseGate.connect(analyser); // 简单的噪声门逻辑 const dataArray = new Uint8Array(analyser.frequencyBinCount); const checkNoise = () => { analyser.getByteFrequencyData(dataArray); const average = dataArray.reduce((a, b) => a + b) / dataArray.length; // 如果平均音量低于阈值,关闭增益 if (average < 10) { // 阈值需要根据实际情况调整 noiseGate.gain.setTargetAtTime(0.01, this.audioContext.currentTime, 0.1); } else { noiseGate.gain.setTargetAtTime(1, this.audioContext.currentTime, 0.05); } requestAnimationFrame(checkNoise); }; checkNoise(); return { input: inputNode, output: analyser, update: () => {} // 简化实现,不提供参数更新 }; } }; } } 六、播客制作专用功能 6.1 片头片尾模板系统 // WordPress后端:片头片尾模板管理 class PodcastTemplateManager { // 注册片头片尾模板自定义文章类型 public function register_template_cpt() { $args = array( 'label' => '播客模板', 'public' => false, 'show_ui' => true, 'show_in_menu' => 'edit.php?post_type=audio_project', 'capability_type' => 'post', 'hierarchical' => false, 'supports' => array('title', 'thumbnail'), 'show_in_rest' => true, ); register_post_type('podcast_template', $args); // 注册模板类型分类 register_taxonomy('template_type', 'podcast_template', array( 'label' => '模板类型', 'hierarchical' => true, 'show_in_rest' => true, 'terms' => array('intro', 'outro', 'ad_break', 'transition') )); } // 获取可用模板 public function get_templates($type = '') { $args = array( 'post_type' => 'podcast_template', 'posts_per_page' => -1, 'post_status' => 'publish' ); if ($type) { $args['tax_query'] = array( array( 'taxonomy' => 'template_type', 'field' => 'slug', 'terms' => $type ) ); } $templates = get_posts($args); $result = array(); foreach ($templates as $template) {

发表评论

WordPress集成教程,连接航空航班动态API实现实时信息查询

WordPress集成教程:连接航空航班动态API实现实时信息查询 引言:为什么要在WordPress中集成航班动态功能? 在当今数字化时代,网站已不仅仅是信息展示平台,更是功能服务的载体。对于旅游类网站、企业差旅管理平台或航空公司相关网站而言,提供实时航班动态查询功能可以显著提升用户体验和网站实用性。WordPress作为全球最流行的内容管理系统,通过代码二次开发可以轻松集成各类API功能,将专业数据服务转化为网站实用工具。 本教程将详细指导您如何在WordPress中连接航空航班动态API,实现实时航班信息查询功能。我们将从API选择、开发环境搭建、代码实现到前端展示,一步步构建一个完整的航班查询工具。无论您是WordPress开发者、网站管理员还是对Web开发感兴趣的爱好者,都能通过本教程掌握将第三方API集成到WordPress中的核心技能。 第一章:准备工作与环境配置 1.1 选择合适的航空航班动态API 在开始开发前,首先需要选择一个可靠且功能完善的航班动态API服务。目前市场上有多种选择: 航空数据API:如FlightAware、FlightStats、AviationStack等专业服务 综合性旅行API:如Amadeus、Sabre等提供的航班状态接口 免费API选项:部分服务提供有限的免费调用额度,适合小型网站 对于本教程,我们选择使用AviationStack API,因为它提供免费套餐(每月500次请求),且文档清晰、数据准确。您需要前往其官网注册账户并获取API密钥。 1.2 WordPress开发环境搭建 确保您已具备以下环境: WordPress安装:建议使用最新版本的WordPress(5.8+) 开发主题或子主题:建议创建子主题进行开发,避免主题更新丢失修改 代码编辑器:如VS Code、Sublime Text或PHPStorm 本地开发环境:可使用XAMPP、MAMP或Local by Flywheel 基本PHP和JavaScript知识 1.3 创建插件结构 我们将创建一个独立的WordPress插件来实现航班查询功能,这样可以确保功能独立且易于维护。 在wp-content/plugins/目录下创建新文件夹flight-status-checker,并在其中创建以下文件结构: flight-status-checker/ ├── flight-status-checker.php # 主插件文件 ├── includes/ │ ├── class-api-handler.php # API处理类 │ ├── class-shortcode.php # 短代码类 │ └── class-admin-settings.php # 管理设置类 ├── assets/ │ ├── css/ │ │ └── frontend.css # 前端样式 │ └── js/ │ └── frontend.js # 前端交互脚本 ├── templates/ │ └── flight-form.php # 查询表单模板 └── languages/ # 国际化文件(可选) 第二章:创建WordPress插件框架 2.1 主插件文件配置 打开flight-status-checker.php,添加插件基本信息: <?php /** * Plugin Name: 航班动态查询工具 * Plugin URI: https://yourwebsite.com/flight-status-checker * Description: 在WordPress网站中集成实时航班动态查询功能 * Version: 1.0.0 * Author: 您的名称 * Author URI: https://yourwebsite.com * License: GPL v2 or later * Text Domain: flight-status-checker * Domain Path: /languages */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('FSC_VERSION', '1.0.0'); define('FSC_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('FSC_PLUGIN_URL', plugin_dir_url(__FILE__)); define('FSC_API_CACHE_TIME', 300); // API缓存时间(秒) // 检查必要扩展 add_action('admin_init', function() { if (!extension_loaded('curl')) { add_action('admin_notices', function() { echo '<div class="notice notice-error"><p>航班动态查询插件需要cURL扩展支持。请联系您的主机提供商启用cURL。</p></div>'; }); } }); // 自动加载类文件 spl_autoload_register(function($class_name) { $prefix = 'FSC_'; $base_dir = FSC_PLUGIN_DIR . 'includes/'; // 检查类是否使用我们的前缀 $len = strlen($prefix); if (strncmp($prefix, $class_name, $len) !== 0) { return; } $relative_class = substr($class_name, $len); $file = $base_dir . 'class-' . strtolower(str_replace('_', '-', $relative_class)) . '.php'; if (file_exists($file)) { require_once $file; } }); // 初始化插件 add_action('plugins_loaded', 'fsc_init_plugin'); function fsc_init_plugin() { // 加载文本域用于国际化 load_plugin_textdomain('flight-status-checker', false, dirname(plugin_basename(__FILE__)) . '/languages'); // 初始化各个组件 if (is_admin()) { new FSC_Admin_Settings(); } new FSC_API_Handler(); new FSC_Shortcode(); // 注册激活/停用钩子 register_activation_hook(__FILE__, 'fsc_activate_plugin'); register_deactivation_hook(__FILE__, 'fsc_deactivate_plugin'); } function fsc_activate_plugin() { // 创建必要的数据库表或选项 if (!get_option('fsc_settings')) { $default_settings = array( 'api_key' => '', 'cache_enabled' => true, 'default_airline' => '', 'date_format' => 'Y-m-d', 'time_format' => 'H:i' ); add_option('fsc_settings', $default_settings); } // 设置定时任务清理缓存 if (!wp_next_scheduled('fsc_clear_cache_daily')) { wp_schedule_event(time(), 'daily', 'fsc_clear_cache_daily'); } } function fsc_deactivate_plugin() { // 清理定时任务 wp_clear_scheduled_hook('fsc_clear_cache_daily'); // 可选:清理缓存数据 // fsc_clear_all_cache(); } // 添加清理缓存的每日任务 add_action('fsc_clear_cache_daily', 'fsc_clear_old_cache'); function fsc_clear_old_cache() { global $wpdb; $table_name = $wpdb->prefix . 'options'; $time = time() - (7 * 24 * 60 * 60); // 删除7天前的缓存 $wpdb->query( $wpdb->prepare( "DELETE FROM $table_name WHERE option_name LIKE %s AND option_value < %d", '_transient_fsc_flight_%', $time ) ); } 第三章:API处理类实现 3.1 创建API处理类 在includes/class-api-handler.php中创建API处理核心类: <?php class FSC_API_Handler { private $api_key; private $api_endpoint = 'http://api.aviationstack.com/v1/'; private $settings; public function __construct() { $this->settings = get_option('fsc_settings', array()); $this->api_key = isset($this->settings['api_key']) ? $this->settings['api_key'] : ''; // 注册AJAX处理函数 add_action('wp_ajax_fsc_get_flight_status', array($this, 'ajax_get_flight_status')); add_action('wp_ajax_nopriv_fsc_get_flight_status', array($this, 'ajax_get_flight_status')); // 注册REST API端点(可选) add_action('rest_api_init', array($this, 'register_rest_routes')); } /** * 获取航班状态 */ public function get_flight_status($params) { // 参数验证 $flight_number = isset($params['flight_number']) ? sanitize_text_field($params['flight_number']) : ''; $airline_iata = isset($params['airline']) ? sanitize_text_field($params['airline']) : ''; $date = isset($params['date']) ? sanitize_text_field($params['date']) : date('Y-m-d'); if (empty($flight_number)) { return array( 'success' => false, 'message' => __('请输入航班号', 'flight-status-checker') ); } // 检查缓存 $cache_key = 'fsc_flight_' . md5($flight_number . $airline_iata . $date); $cached_data = get_transient($cache_key); if ($cached_data !== false && isset($this->settings['cache_enabled']) && $this->settings['cache_enabled']) { return $cached_data; } // 构建API请求参数 $api_params = array( 'access_key' => $this->api_key, 'flight_iata' => $flight_number, 'flight_date' => $date ); if (!empty($airline_iata)) { $api_params['airline_iata'] = $airline_iata; } // 发送API请求 $response = $this->make_api_request('flights', $api_params); if ($response['success']) { // 处理API响应数据 $processed_data = $this->process_flight_data($response['data']); // 缓存结果 if (isset($this->settings['cache_enabled']) && $this->settings['cache_enabled']) { set_transient($cache_key, $processed_data, FSC_API_CACHE_TIME); } return $processed_data; } else { return array( 'success' => false, 'message' => $response['message'] ); } } /** * 发送API请求 */ private function make_api_request($endpoint, $params) { if (empty($this->api_key)) { return array( 'success' => false, 'message' => __('API密钥未配置,请在插件设置中配置API密钥。', 'flight-status-checker') ); } $url = $this->api_endpoint . $endpoint . '?' . http_build_query($params); // 使用WordPress HTTP API $response = wp_remote_get($url, array( 'timeout' => 15, 'sslverify' => false )); if (is_wp_error($response)) { return array( 'success' => false, 'message' => $response->get_error_message() ); } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if (isset($data['error'])) { return array( 'success' => false, 'message' => isset($data['error']['message']) ? $data['error']['message'] : __('API请求失败', 'flight-status-checker') ); } if (isset($data['data']) && is_array($data['data']) && count($data['data']) > 0) { return array( 'success' => true, 'data' => $data['data'] ); } else { return array( 'success' => false, 'message' => __('未找到航班信息', 'flight-status-checker') ); } } /** * 处理航班数据 */ private function process_flight_data($flight_data) { if (empty($flight_data) || !is_array($flight_data)) { return array( 'success' => false, 'message' => __('航班数据格式错误', 'flight-status-checker') ); } $flight = $flight_data[0]; // 取第一个结果 // 格式化数据 $formatted_data = array( 'success' => true, 'flight' => array( 'number' => isset($flight['flight']['iata']) ? $flight['flight']['iata'] : '', 'airline' => isset($flight['airline']['name']) ? $flight['airline']['name'] : '', 'status' => isset($flight['flight_status']) ? $this->translate_flight_status($flight['flight_status']) : '', 'departure' => array( 'airport' => isset($flight['departure']['airport']) ? $flight['departure']['airport'] : '', 'iata' => isset($flight['departure']['iata']) ? $flight['departure']['iata'] : '', 'scheduled' => isset($flight['departure']['scheduled']) ? $this->format_datetime($flight['departure']['scheduled']) : '', 'estimated' => isset($flight['departure']['estimated']) ? $this->format_datetime($flight['departure']['estimated']) : '', 'actual' => isset($flight['departure']['actual']) ? $this->format_datetime($flight['departure']['actual']) : '', 'gate' => isset($flight['departure']['gate']) ? $flight['departure']['gate'] : '--', 'terminal' => isset($flight['departure']['terminal']) ? $flight['departure']['terminal'] : '--' ), 'arrival' => array( 'airport' => isset($flight['arrival']['airport']) ? $flight['arrival']['airport'] : '', 'iata' => isset($flight['arrival']['iata']) ? $flight['arrival']['iata'] : '', 'scheduled' => isset($flight['arrival']['scheduled']) ? $this->format_datetime($flight['arrival']['scheduled']) : '', 'estimated' => isset($flight['arrival']['estimated']) ? $this->format_datetime($flight['arrival']['estimated']) : '', 'actual' => isset($flight['arrival']['actual']) ? $this->format_datetime($flight['arrival']['actual']) : '', 'gate' => isset($flight['arrival']['gate']) ? $flight['arrival']['gate'] : '--', 'terminal' => isset($flight['arrival']['terminal']) ? $flight['arrival']['terminal'] : '--' ), 'aircraft' => isset($flight['aircraft']['iata']) ? $flight['aircraft']['iata'] : '--', 'live' => isset($flight['live']) ? $flight['live'] : null ) ); return $formatted_data; } /** * 翻译航班状态 */ private function translate_flight_status($status) { $status_map = array( 'scheduled' => '计划中', 'active' => '飞行中', 'landed' => '已降落', 'cancelled' => '已取消', 'incident' => '异常', 'diverted' => '已改航' ); return isset($status_map[$status]) ? $status_map[$status] : $status; } /** * 格式化日期时间 */ private function format_datetime($datetime_string) { if (empty($datetime_string)) { return '--'; } $date_format = isset($this->settings['date_format']) ? $this->settings['date_format'] : 'Y-m-d'; $time_format = isset($this->settings['time_format']) ? $this->settings['time_format'] : 'H:i'; $timestamp = strtotime($datetime_string); return date($date_format . ' ' . $time_format, $timestamp); } /** * AJAX处理函数 */ public function ajax_get_flight_status() { // 验证nonce if (!check_ajax_referer('fsc_ajax_nonce', 'nonce', false)) { wp_die(json_encode(array( 'success' => false, 'message' => __('安全验证失败', 'flight-status-checker') ))); } $params = array( 'flight_number' => isset($_POST['flight_number']) ? sanitize_text_field($_POST['flight_number']) : '', 'airline' => isset($_POST['airline']) ? sanitize_text_field($_POST['airline']) : '', 'date' => isset($_POST['date']) ? sanitize_text_field($_POST['date']) : date('Y-m-d') ); $result = $this->get_flight_status($params); wp_send_json($result); } /** * 注册REST API路由 */ public function register_rest_routes() { register_rest_route('flight-status/v1', '/check', array( 'methods' => 'GET', 'callback' => array($this, 'rest_get_flight_status'), 'permission_callback' => '__return_true', 'args' => array( 'flight_number' => array( 'required' => true, 'validate_callback' => function($param) { return !empty($param); } ), 'date' => array( 'required' => false, 'default' => date('Y-m-d') ) ) )); } public function rest_get_flight_status($request) { $params = $request->get_params(); $result = $this->get_flight_status($params); return rest_ensure_response($result); } } 第四章:创建短代码和前端界面 4.1 短代码类实现 在includes/class-shortcode.php中创建短代码处理类: <?php class FSC_Shortcode { public function __construct() { add_shortcode('flight_status_checker', array($this, 'render_shortcode')); // 注册前端脚本和样式 add_action('wp_enqueue_scripts', array($this, 'enqueue_frontend_assets')); } /** * 渲染短代码 */ public function render_shortcode($atts) { // 短代码属性 $atts = shortcode_atts(array( 'title' => '航班动态查询', 'show_airline_field' => 'yes', 'default_date' => '', 'button_text' => '查询航班状态', 'layout' => 'default' // default, compact, detailed ), $atts, 'flight_status_checker'); // 获取模板 ob_start(); include FSC_PLUGIN_DIR . 'templates/flight-form.php'; return ob_get_clean(); } /** * 加载前端资源 */ public function enqueue_frontend_assets() { // 只在需要时加载 global $post; if (is_a($post, 'WP_Post') && has_shortcode($post->post_content, 'flight_status_checker')) { // 加载CSS wp_enqueue_style( 'fsc-frontend-style', FSC_PLUGIN_URL . 'assets/css/frontend.css', array(), FSC_VERSION ); // 加载JavaScript wp_enqueue_script( 'fsc-frontend-script', FSC_PLUGIN_URL . 'assets/js/frontend.js', array('jquery'), FSC_VERSION, true ); // 本地化脚本 wp_localize_script('fsc-frontend-script', 'fsc_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('fsc_ajax_nonce'), 'loading_text' => __('查询中...', 'flight-status-checker'), 'error_messages' => array( 'flight_required' => __('请输入航班号', 'flight-status-checker'), 'invalid_format' => __('航班号格式不正确', 'flight-status-checker') ) )); } } } 4.2 创建查询表单模板 在templates/flight-form.php中创建前端表单: <div class="fsc-flight-checker-wrapper layout-<?php echo esc_attr($atts['layout']); ?>"> <div class="fsc-header"> <h3><?php echo esc_html($atts['title']); ?></h3> <p class="fsc-description"><?php _e('输入航班号查询实时航班动态信息', 'flight-status-checker'); ?></p> </div> <div class="fsc-form-container"> <form id="fsc-flight-form" method="post"> <div class="fsc-form-row"> <div class="fsc-form-group"> <label for="fsc-flight-number"> <i class="fsc-icon fsc-icon-flight"></i> <?php _e('航班号', 'flight-status-checker'); ?> </label> <input type="text" id="fsc-flight-number" name="flight_number" placeholder="例如:CA123" required pattern="[A-Za-z]{2}d{1,4}" title="<?php _e('请输入正确的航班号格式,如:CA123', 'flight-status-checker'); ?>" > <small class="fsc-hint"><?php _e('格式:航空公司代码+数字,如:CA123, MU586', 'flight-status-checker'); ?></small> </div> <?php if ($atts['show_airline_field'] === 'yes') : ?> <div class="fsc-form-group"> <label for="fsc-airline"> <i class="fsc-icon fsc-icon-airline"></i> <?php _e('航空公司代码(可选)', 'flight-status-checker'); ?> </label> <input type="text" id="fsc-airline" name="airline" placeholder="例如:CA" pattern="[A-Za-z]{2}" title="<?php _e('请输入2位航空公司代码', 'flight-status-checker'); ?>" > <small class="fsc-hint"><?php _e('2位IATA航空公司代码', 'flight-status-checker'); ?></small> </div> <?php endif; ?> <div class="fsc-form-group"> <label for="fsc-date"> <i class="fsc-icon fsc-icon-calendar"></i> <?php _e('航班日期', 'flight-status-checker'); ?> </label> <input type="date" id="fsc-date" name="date" value="<?php echo empty($atts['default_date']) ? date('Y-m-d') : esc_attr($atts['default_date']); ?>" min="<?php echo date('Y-m-d', strtotime('-30 days')); ?>" max="<?php echo date('Y-m-d', strtotime('+30 days')); ?>" > </div> </div> <div class="fsc-form-actions"> <button type="submit" class="fsc-submit-btn"> <i class="fsc-icon fsc-icon-search"></i> <?php echo esc_html($atts['button_text']); ?> </button> <button type="button" class="fsc-reset-btn"> <?php _e('重置', 'flight-status-checker'); ?> </button> </div> </form> </div> <div class="fsc-results-container" style="display: none;"> <div class="fsc-loading"> <div class="fsc-spinner"></div> <p><?php _e('正在查询航班信息...', 'flight-status-checker'); ?></p> </div> <div class="fsc-results" style="display: none;"> <!-- 结果将通过JavaScript动态填充 --> </div> <div class="fsc-error" style="display: none;"> <!-- 错误信息将通过JavaScript动态填充 --> </div> </div> <div class="fsc-footer"> <p class="fsc-disclaimer"> <small> <i class="fsc-icon fsc-icon-info"></i> <?php _e('数据来源:AviationStack API,信息仅供参考,请以航空公司官方信息为准。', 'flight-status-checker'); ?> </small> </p> </div> </div> 4.3 前端样式设计 在assets/css/frontend.css中创建样式: /* 航班查询工具主样式 */ .fsc-flight-checker-wrapper { max-width: 800px; margin: 2rem auto; padding: 2rem; background: #ffffff; border-radius: 12px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; } .fsc-header { text-align: center; margin-bottom: 2rem; padding-bottom: 1.5rem; border-bottom: 2px solid #f0f0f0; } .fsc-header h3 { color: #1a237e; margin: 0 0 0.5rem 0; font-size: 1.8rem; font-weight: 600; } .fsc-description { color: #666; margin: 0; font-size: 1rem; } /* 表单样式 */ .fsc-form-container { margin-bottom: 2rem; } .fsc-form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1.5rem; margin-bottom: 1.5rem; } .fsc-form-group { display: flex; flex-direction: column; } .fsc-form-group label { display: flex; align-items: center; margin-bottom: 0.5rem; color: #333; font-weight: 500; font-size: 0.95rem; } .fsc-icon { margin-right: 0.5rem; font-size: 1.1rem; color: #1a237e; } .fsc-form-group input { padding: 0.75rem 1rem; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 1rem; transition: all 0.3s ease; background: #fafafa; } .fsc-form-group input:focus { outline: none; border-color: #1a237e; background: #fff; box-shadow: 0 0 0 3px rgba(26, 35, 126, 0.1); } .fsc-hint { margin-top: 0.25rem; color: #666; font-size: 0.85rem; line-height: 1.3; } /* 按钮样式 */ .fsc-form-actions { display: flex; gap: 1rem; justify-content: center; margin-top: 1.5rem; } .fsc-submit-btn, .fsc-reset-btn { padding: 0.875rem 2rem; border: none; border-radius: 8px; font-size: 1rem; font-weight: 500; cursor: pointer; transition: all 0.3s ease; display: flex; align-items: center; justify-content: center; gap: 0.5rem; } .fsc-submit-btn { background: linear-gradient(135deg, #1a237e 0%, #283593 100%); color: white; min-width: 180px; } .fsc-submit-btn:hover { background: linear-gradient(135deg, #283593 0%, #303f9f 100%); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(26, 35, 126, 0.3); } .fsc-reset-btn { background: #f5f5f5; color: #666; border: 2px solid #e0e0e0; } .fsc-reset-btn:hover { background: #e0e0e0; color: #333; } /* 加载状态 */ .fsc-loading { text-align: center; padding: 3rem; } .fsc-spinner { width: 50px; height: 50px; border: 4px solid #f3f3f3; border-top: 4px solid #1a237e; border-radius: 50%; animation: fsc-spin 1s linear infinite; margin: 0 auto 1rem; } @keyframes fsc-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .fsc-loading p { color: #666; margin: 0; } /* 结果展示 */ .fsc-results { background: #f8f9ff; border-radius: 10px; padding: 2rem; margin-top: 1.5rem; border: 1px solid #e0e7ff; } .fsc-flight-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; padding-bottom: 1rem; border-bottom: 2px solid #e0e7ff; } .fsc-flight-number { font-size: 1.8rem; font-weight: 700; color: #1a237e; margin: 0; } .fsc-flight-status { padding: 0.5rem 1.5rem; border-radius: 20px; font-weight: 600; font-size: 0.9rem; } .fsc-status-scheduled { background: #e3f2fd; color: #1565c0; } .fsc-status-active { background: #e8f5e9; color: #2e7d32; } .fsc-status-landed { background: #f1f8e9; color: #558b2f; } .fsc-status-cancelled { background: #ffebee; color: #c62828; } .fsc-status-delayed { background: #fff3e0; color: #ef6c00; } .fsc-flight-details { display: grid; grid-template-columns: 1fr auto 1fr; gap: 2rem; align-items: center; margin-bottom: 2rem; } .fsc-departure, .fsc-arrival { padding: 1.5rem; background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); } .fsc-airport-name { font-size: 1.3rem; font-weight: 600; color: #333; margin: 0 0 0.5rem 0; } .fsc-airport-code { color: #666; font-size: 0.9rem; margin-bottom: 1rem; } .fsc-time-group { margin-bottom: 1rem; } .fsc-time-label { display: block; color: #888; font-size: 0.85rem; margin-bottom: 0.25rem; } .fsc-time-value { font-size: 1.1rem; font-weight: 500; color: #333; } .fsc-terminal-info { display: flex; gap: 1rem; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #eee; } .fsc-terminal-info span { font-size: 0.9rem; color: #666; } .fsc-flight-path { text-align: center; position: relative; } .fsc-flight-path::before { content: ''; position: absolute; top: 50%; left: 0; right: 0; height: 2px; background: linear-gradient(90deg, #1a237e, #283593); z-index: 1; } .fsc-flight-path i { position: relative; z-index: 2; background: white; padding: 0 1rem; color: #1a237e; font-size: 1.5rem; } .fsc-flight-meta { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; padding: 1.5rem; background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); } .fsc-meta-item { text-align: center; } .fsc-meta-label { display: block; color: #888; font-size: 0.85rem; margin-bottom: 0.25rem; } .fsc-meta-value { font-size: 1.1rem; font-weight: 500; color: #333; } /* 错误信息 */ .fsc-error { padding: 2rem; text-align: center; background: #ffebee; border-radius: 8px; border-left: 4px solid #c62828; } .fsc-error-icon { font-size: 3rem; color: #c62828; margin-bottom: 1rem; } .fsc-error-title { color: #c62828; margin: 0 0 0.5rem 0; font-size: 1.2rem; } .fsc-error-message { color: #666; margin: 0; } /* 页脚 */ .fsc-footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid #f0f0f0; } .fsc-disclaimer { color: #888; font-size: 0.85rem; line-height: 1.5; margin: 0; display: flex; align-items: flex-start; gap: 0.5rem; } /* 响应式设计 */ @media (max-width: 768px) { .fsc-flight-checker-wrapper { padding: 1.5rem; margin: 1rem auto; } .fsc-form-row { grid-template-columns: 1fr; gap: 1rem; } .fsc-flight-details { grid-template-columns: 1fr; gap: 1rem; } .fsc-flight-path { display: none; } .fsc-form-actions { flex-direction: column; } .fsc-submit-btn, .fsc-reset-btn { width: 100%; } } /* 紧凑布局 */ .fsc-flight-checker-wrapper.layout-compact { max-width: 500px; padding: 1.5rem; } .fsc-flight-checker-wrapper.layout-compact .fsc-form-row { grid-template-columns: 1fr; } .fsc-flight-checker-wrapper.layout-compact .fsc-results { padding: 1rem; } /* 详细布局 */ .fsc-flight-checker-wrapper.layout-detailed .fsc-flight-details { grid-template-columns: 1fr auto 1fr; } .fsc-flight-checker-wrapper.layout-detailed .fsc-flight-meta { grid-template-columns: repeat(4, 1fr); } 4.4 前端交互脚本 在`assets

发表评论

详细教程,为WordPress网站开发会议室预订与资源调度管理系统

WordPress会议室预订与资源调度管理系统开发详细教程 引言:为什么需要会议室预订与资源调度系统 在现代办公环境中,会议室和各种共享资源(如投影仪、车辆、设备等)的高效管理是企业运营的重要环节。传统的人工预约方式不仅效率低下,还容易引发时间冲突和资源浪费。随着远程办公和混合工作模式的普及,一个数字化的资源调度系统变得尤为重要。 WordPress作为全球最流行的内容管理系统,不仅适用于博客和网站建设,通过代码二次开发,完全可以实现会议室预订与资源调度这样的专业功能。本教程将详细指导您如何从零开始,在WordPress平台上开发一个功能完善的会议室预订与资源调度管理系统。 第一部分:系统需求分析与规划 1.1 核心功能需求 在开始开发之前,我们需要明确系统应具备的核心功能: 会议室管理:添加、编辑、删除会议室,设置容量、设备配置等信息 资源管理:管理可预订资源(投影仪、白板、视频会议设备等) 预订功能:用户可查看可用时间段并进行预订 冲突检测:自动检测时间冲突,避免重复预订 日历视图:直观展示会议室和资源的占用情况 用户权限管理:不同用户角色拥有不同权限 通知系统:预订确认、提醒、变更通知 报表统计:使用频率统计、资源利用率分析 1.2 技术架构设计 我们将采用以下技术架构: 前端:HTML5、CSS3、JavaScript(jQuery)、FullCalendar.js 后端:PHP(WordPress核心)、MySQL数据库 通信:AJAX实现无刷新操作 安全性:WordPress非ces、数据验证、权限检查 1.3 数据库设计 我们需要创建以下自定义数据表: wp_meeting_rooms:存储会议室信息 wp_resources:存储可预订资源信息 wp_bookings:存储预订记录 wp_booking_resources:预订与资源的关联表 第二部分:开发环境搭建与基础配置 2.1 开发环境准备 首先确保您具备以下环境: WordPress 5.0+ 安装 PHP 7.4+ 版本 MySQL 5.6+ 数据库 代码编辑器(VS Code、Sublime Text等) 2.2 创建自定义插件 我们将创建一个独立的插件来实现所有功能,确保与主题分离,便于维护和迁移。 在WordPress的wp-content/plugins/目录下创建新文件夹meeting-room-booking-system,并在其中创建主插件文件: <?php /** * Plugin Name: 会议室预订与资源调度管理系统 * Plugin URI: https://yourwebsite.com/ * Description: 一个功能完整的会议室与资源预订管理系统 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later * Text Domain: mr-booking */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('MRB_PLUGIN_PATH', plugin_dir_path(__FILE__)); define('MRB_PLUGIN_URL', plugin_dir_url(__FILE__)); define('MRB_VERSION', '1.0.0'); // 初始化插件 require_once MRB_PLUGIN_PATH . 'includes/class-init.php'; 2.3 创建数据库表 在插件初始化类中,添加创建数据库表的代码: class MRB_Init { public function __construct() { // 激活插件时创建表 register_activation_hook(__FILE__, array($this, 'create_tables')); // 加载其他组件 $this->load_dependencies(); } public function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); // 会议室表 $rooms_table = $wpdb->prefix . 'meeting_rooms'; $rooms_sql = "CREATE TABLE IF NOT EXISTS $rooms_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL, description text, capacity smallint NOT NULL DEFAULT 10, location varchar(200), amenities text, status tinyint(1) DEFAULT 1, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) $charset_collate;"; // 资源表 $resources_table = $wpdb->prefix . 'resources'; $resources_sql = "CREATE TABLE IF NOT EXISTS $resources_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL, type varchar(50) NOT NULL, description text, quantity smallint NOT NULL DEFAULT 1, status tinyint(1) DEFAULT 1, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) $charset_collate;"; // 预订表 $bookings_table = $wpdb->prefix . 'bookings'; $bookings_sql = "CREATE TABLE IF NOT EXISTS $bookings_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, room_id mediumint(9) NOT NULL, user_id bigint(20) NOT NULL, title varchar(200) NOT NULL, description text, start_time datetime NOT NULL, end_time datetime NOT NULL, attendees smallint DEFAULT 1, status varchar(20) DEFAULT 'confirmed', created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY room_id (room_id), KEY user_id (user_id), KEY start_time (start_time), KEY end_time (end_time) ) $charset_collate;"; // 预订资源关联表 $booking_resources_table = $wpdb->prefix . 'booking_resources'; $booking_resources_sql = "CREATE TABLE IF NOT EXISTS $booking_resources_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, booking_id mediumint(9) NOT NULL, resource_id mediumint(9) NOT NULL, quantity smallint NOT NULL DEFAULT 1, PRIMARY KEY (id), KEY booking_id (booking_id), KEY resource_id (resource_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($rooms_sql); dbDelta($resources_sql); dbDelta($bookings_sql); dbDelta($booking_resources_sql); } private function load_dependencies() { // 加载其他类文件 require_once MRB_PLUGIN_PATH . 'includes/class-admin.php'; require_once MRB_PLUGIN_PATH . 'includes/class-frontend.php'; require_once MRB_PLUGIN_PATH . 'includes/class-ajax.php'; require_once MRB_PLUGIN_PATH . 'includes/class-shortcodes.php'; } } new MRB_Init(); 第三部分:后台管理界面开发 3.1 创建管理菜单 在class-admin.php中,添加管理菜单功能: class MRB_Admin { public function __construct() { add_action('admin_menu', array($this, 'add_admin_menus')); add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts')); } public function add_admin_menus() { // 主菜单 add_menu_page( '会议室预订系统', '会议室预订', 'manage_options', 'mrb-dashboard', array($this, 'dashboard_page'), 'dashicons-calendar-alt', 30 ); // 子菜单 add_submenu_page( 'mrb-dashboard', '会议室管理', '会议室', 'manage_options', 'mrb-rooms', array($this, 'rooms_page') ); add_submenu_page( 'mrb-dashboard', '资源管理', '资源', 'manage_options', 'mrb-resources', array($this, 'resources_page') ); add_submenu_page( 'mrb-dashboard', '预订管理', '预订', 'manage_options', 'mrb-bookings', array($this, 'bookings_page') ); add_submenu_page( 'mrb-dashboard', '系统设置', '设置', 'manage_options', 'mrb-settings', array($this, 'settings_page') ); } public function enqueue_admin_scripts($hook) { // 仅在我们的插件页面加载脚本 if (strpos($hook, 'mrb-') !== false) { wp_enqueue_style('mrb-admin-style', MRB_PLUGIN_URL . 'assets/css/admin.css', array(), MRB_VERSION); wp_enqueue_script('mrb-admin-script', MRB_PLUGIN_URL . 'assets/js/admin.js', array('jquery'), MRB_VERSION, true); // 本地化脚本,传递数据到JS wp_localize_script('mrb-admin-script', 'mrb_admin', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('mrb_admin_nonce') )); } } public function dashboard_page() { include MRB_PLUGIN_PATH . 'templates/admin/dashboard.php'; } public function rooms_page() { include MRB_PLUGIN_PATH . 'templates/admin/rooms.php'; } public function resources_page() { include MRB_PLUGIN_PATH . 'templates/admin/resources.php'; } public function bookings_page() { include MRB_PLUGIN_PATH . 'templates/admin/bookings.php'; } public function settings_page() { include MRB_PLUGIN_PATH . 'templates/admin/settings.php'; } } 3.2 会议室管理界面 创建templates/admin/rooms.php文件: <div class="wrap mrb-admin-wrap"> <h1 class="wp-heading-inline">会议室管理</h1> <a href="#" class="page-title-action" id="mrb-add-room">添加新会议室</a> <hr class="wp-header-end"> <!-- 添加/编辑会议室表单 (默认隐藏) --> <div id="mrb-room-form-container" style="display:none;"> <h2 id="mrb-form-title">添加会议室</h2> <form id="mrb-room-form" method="post"> <?php wp_nonce_field('mrb_save_room', 'mrb_room_nonce'); ?> <input type="hidden" id="room_id" name="room_id" value="0"> <table class="form-table"> <tr> <th scope="row"><label for="room_name">会议室名称</label></th> <td><input type="text" id="room_name" name="room_name" class="regular-text" required></td> </tr> <tr> <th scope="row"><label for="room_capacity">容量</label></th> <td><input type="number" id="room_capacity" name="room_capacity" min="1" max="500" value="10" required></td> </tr> <tr> <th scope="row"><label for="room_location">位置</label></th> <td><input type="text" id="room_location" name="room_location" class="regular-text"></td> </tr> <tr> <th scope="row"><label for="room_description">描述</label></th> <td><textarea id="room_description" name="room_description" rows="5" class="large-text"></textarea></td> </tr> <tr> <th scope="row"><label for="room_amenities">设备配置</label></th> <td> <textarea id="room_amenities" name="room_amenities" rows="3" class="large-text" placeholder="例如:投影仪、白板、视频会议系统"></textarea> <p class="description">每行一个设备,或使用逗号分隔</p> </td> </tr> <tr> <th scope="row"><label for="room_status">状态</label></th> <td> <select id="room_status" name="room_status"> <option value="1">可用</option> <option value="0">不可用</option> </select> </td> </tr> </table> <p class="submit"> <button type="submit" class="button button-primary">保存会议室</button> <button type="button" class="button" id="mrb-cancel-form">取消</button> </p> </form> </div> <!-- 会议室列表 --> <div id="mrb-rooms-list"> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <th scope="col" width="5%">ID</th> <th scope="col" width="20%">名称</th> <th scope="col" width="10%">容量</th> <th scope="col" width="20%">位置</th> <th scope="col" width="25%">设备配置</th> <th scope="col" width="10%">状态</th> <th scope="col" width="10%">操作</th> </tr> </thead> <tbody id="mrb-rooms-table-body"> <!-- 通过AJAX加载数据 --> <tr> <td colspan="7">加载中...</td> </tr> </tbody> </table> </div> </div> 第四部分:前端预订界面开发 4.1 创建前端短代码 在class-shortcodes.php中,创建用于前端显示的短代码: class MRB_Shortcodes { public function __construct() { add_shortcode('meeting_room_booking', array($this, 'booking_calendar_shortcode')); add_shortcode('meeting_room_list', array($this, 'room_list_shortcode')); add_action('wp_enqueue_scripts', array($this, 'enqueue_frontend_scripts')); } public function enqueue_frontend_scripts() { global $post; // 仅在包含我们短代码的页面加载脚本 if (is_a($post, 'WP_Post') && has_shortcode($post->post_content, 'meeting_room_booking')) { // FullCalendar库 wp_enqueue_style('fullcalendar-css', 'https://cdn.jsdelivr.net/npm/fullcalendar@5.10.1/main.min.css', array(), '5.10.1'); wp_enqueue_script('fullcalendar-js', 'https://cdn.jsdelivr.net/npm/fullcalendar@5.10.1/main.min.js', array('jquery'), '5.10.1', true); // 本地化日历 wp_enqueue_script('fullcalendar-locale', 'https://cdn.jsdelivr.net/npm/fullcalendar@5.10.1/locales/zh-cn.js', array('fullcalendar-js'), '5.10.1', true); // 插件前端样式和脚本 wp_enqueue_style('mrb-frontend-style', MRB_PLUGIN_URL . 'assets/css/frontend.css', array(), MRB_VERSION); wp_enqueue_script('mrb-frontend-script', MRB_PLUGIN_URL . 'assets/js/frontend.js', array('jquery', 'fullcalendar-js'), MRB_VERSION, true); // 传递数据到前端JS wp_localize_script('mrb-frontend-script', 'mrb_frontend', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('mrb_frontend_nonce'), 'current_user_id' => get_current_user_id(), 'calendar_locale' => get_locale(), 'time_format' => get_option('time_format', 'H:i'), 'date_format' => get_option('date_format', 'Y-m-d') )); } } public function booking_calendar_shortcode($atts) { // 检查用户是否登录 if (!is_user_logged_in()) { return '<div class="mrb-login-required">请先登录系统以预订会议室。</div>'; } ob_start(); include MRB_PLUGIN_PATH . 'templates/frontend/booking-calendar.php'; return ob_get_clean(); } public function room_list_shortcode($atts) { ob_start(); include MRB_PLUGIN_PATH . 'templates/frontend/room-list.php'; return ob_get_clean(); } } 4.2 预订日历界面 创建templates/frontend/booking-calendar.php: <div class="mrb-booking-container"> <div class="mrb-booking-header"> <h2>会议室预订系统</h2> <div class="mrb-user-info"> 欢迎,<?php echo wp_get_current_user()->display_name; ?>! </div> </div> <div class="mrb-booking-main"> <div class="mrb-sidebar"> <div class="mrb-room-filter"> <h3>筛选会议室</h3> <div class="mrb-filter-section"> <label for="mrb-filter-capacity">最小容量:</label> <select id="mrb-filter-capacity"> <option value="0">不限</option> <option value="5">5人以上</option> <option value="10">10人以上</option> <option value="20">20人以上</option> <option value="30">30人以上</option> <option value="50">50人以上</option> </select> </div> <div class="mrb-filter-section"> <label for="mrb-filter-equipment">设备要求:</label> <div class="mrb-equipment-checkboxes"> <label><input type="checkbox" value="projector"> 投影仪</label> <label><input type="checkbox" value="whiteboard"> 白板</label> <label><input type="checkbox" value="videoconf"> 视频会议</label> <label><input type="checkbox" value="phone"> 电话会议</label> </div> </div> <button id="mrb-apply-filter" class="button">应用筛选</button> <button id="mrb-reset-filter" class="button button-secondary">重置筛选</button> </div> <div class="mrb-room-list"> <h3>可用会议室</h3> <div id="mrb-rooms-container"> <!-- 通过AJAX加载会议室列表 --> <div class="mrb-loading">加载中...</div> </div> </div> <div class="mrb-resource-list"> <h3>可预订资源</h3> <div id="mrb-resources-container"> <!-- 通过AJAX加载资源列表 --> <div class="mrb-loading">加载中...</div> </div> </div> </div> <div class="mrb-calendar-section"> <div class="mrb-calendar-controls"> <button id="mrb-prev-week" class="button">< 上周</button> <button id="mrb-today" class="button">今天</button> <button id="mrb-next-week" class="button">下周 ></button> <span id="mrb-current-week" class="mrb-week-display"></span> <div class="mrb-view-toggle"> <button class="button active" data-view="week">周视图</button> <button class="button" data-view="day">日视图</button> <button class="button" data-view="month">月视图</button> </div> </div> <div id="mrb-booking-calendar"></div> <div class="mrb-legend"> <div class="mrb-legend-item"> <span class="mrb-legend-color available"></span> <span>可预订</span> </div> <div class="mrb-legend-item"> <span class="mrb-legend-color booked"></span> <span>已预订</span> </div> <div class="mrb-legend-item"> <span class="mrb-legend-color your-booking"></span> <span>您的预订</span> </div> <div class="mrb-legend-item"> <span class="mrb-legend-color unavailable"></span> <span>不可用</span> </div> </div> </div> </div> </div> <!-- 预订模态框 --><div id="mrb-booking-modal" class="mrb-modal" style="display:none;"> <div class="mrb-modal-content"> <div class="mrb-modal-header"> <h3>新建预订</h3> <span class="mrb-modal-close">&times;</span> </div> <div class="mrb-modal-body"> <form id="mrb-booking-form"> <?php wp_nonce_field('mrb_create_booking', 'mrb_booking_nonce'); ?> <input type="hidden" id="booking_room_id" name="room_id"> <input type="hidden" id="booking_start" name="start_time"> <input type="hidden" id="booking_end" name="end_time"> <div class="mrb-form-group"> <label for="booking_title">会议主题 *</label> <input type="text" id="booking_title" name="title" required> </div> <div class="mrb-form-group"> <label for="booking_description">会议描述</label> <textarea id="booking_description" name="description" rows="3"></textarea> </div> <div class="mrb-form-row"> <div class="mrb-form-group"> <label for="booking_date">日期</label> <input type="text" id="booking_date" name="date" readonly> </div> <div class="mrb-form-group"> <label for="booking_start_time">开始时间</label> <select id="booking_start_time" name="start_time_select"> <!-- 通过JS动态生成时间选项 --> </select> </div> <div class="mrb-form-group"> <label for="booking_end_time">结束时间</label> <select id="booking_end_time" name="end_time_select"> <!-- 通过JS动态生成时间选项 --> </select> </div> </div> <div class="mrb-form-group"> <label for="booking_attendees">参会人数 *</label> <input type="number" id="booking_attendees" name="attendees" min="1" value="1" required> <span id="booking_capacity_info" class="mrb-hint"></span> </div> <div class="mrb-form-group"> <label>预订资源</label> <div id="mrb-booking-resources"> <!-- 通过JS动态生成资源选项 --> </div> </div> <div class="mrb-form-group"> <label for="booking_recurring">重复预订</label> <select id="booking_recurring" name="recurring"> <option value="none">不重复</option> <option value="daily">每天</option> <option value="weekly">每周</option> <option value="monthly">每月</option> </select> <div id="mrb-recurring-options" style="display:none;"> <label for="booking_recurring_count">重复次数:</label> <input type="number" id="booking_recurring_count" name="recurring_count" min="1" max="12" value="1"> </div> </div> <div class="mrb-form-actions"> <button type="submit" class="button button-primary">确认预订</button> <button type="button" class="button mrb-modal-cancel">取消</button> </div> </form> </div> </div> </div> ## 第五部分:AJAX处理与数据交互 ### 5.1 AJAX处理类 在`class-ajax.php`中,处理所有前端和后端的AJAX请求: class MRB_Ajax { public function __construct() { // 前端AJAX动作 add_action('wp_ajax_mrb_get_rooms', array($this, 'get_rooms')); add_action('wp_ajax_nopriv_mrb_get_rooms', array($this, 'get_rooms')); add_action('wp_ajax_mrb_get_resources', array($this, 'get_resources')); add_action('wp_ajax_nopriv_mrb_get_resources', array($this, 'get_resources')); add_action('wp_ajax_mrb_get_bookings', array($this, 'get_bookings')); add_action('wp_ajax_nopriv_mrb_get_bookings', array($this, 'get_bookings')); add_action('wp_ajax_mrb_create_booking', array($this, 'create_booking')); add_action('wp_ajax_mrb_cancel_booking', array($this, 'cancel_booking')); // 后台AJAX动作 add_action('wp_ajax_mrb_admin_save_room', array($this, 'admin_save_room')); add_action('wp_ajax_mrb_admin_delete_room', array($this, 'admin_delete_room')); add_action('wp_ajax_mrb_admin_get_rooms', array($this, 'admin_get_rooms')); } /** * 获取会议室列表 */ public function get_rooms() { // 验证nonce if (!check_ajax_referer('mrb_frontend_nonce', 'nonce', false)) { wp_die('安全验证失败', 403); } global $wpdb; $table_name = $wpdb->prefix . 'meeting_rooms'; // 获取筛选参数 $capacity = isset($_POST['capacity']) ? intval($_POST['capacity']) : 0; $equipment = isset($_POST['equipment']) ? $_POST['equipment'] : array(); // 构建查询 $where = array('status = 1'); $params = array(); if ($capacity > 0) { $where[] = 'capacity >= %d'; $params[] = $capacity; } $where_sql = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : ''; // 如果有设备筛选,需要进一步处理 if (!empty($equipment) && is_array($equipment)) { $query = "SELECT * FROM $table_name $where_sql"; if (!empty($params)) { $query = $wpdb->prepare($query, $params); } $rooms = $wpdb->get_results($query); // 过滤设备 $filtered_rooms = array(); foreach ($rooms as $room) { $amenities = explode(',', $room->amenities); $amenities = array_map('trim', $amenities); $match = true; foreach ($equipment as $eq) { if (!in_array($eq, $amenities)) { $match = false; break; } } if ($match) { $filtered_rooms[] = $room; } } $rooms = $filtered_rooms; } else { $query = "SELECT * FROM $table_name $where_sql ORDER BY name ASC"; if (!empty($params)) { $query = $wpdb->prepare($query, $params); } $rooms = $wpdb->get_results($query); } // 格式化返回数据 $formatted_rooms = array(); foreach ($rooms as $room) { $formatted_rooms[] = array( 'id' => $room->id, 'name' => $room->name, 'capacity' => $room->capacity, 'location' => $room->location, 'amenities' => $room->amenities, 'description' => $room->description ); } wp_send_json_success($formatted_rooms); } /** * 获取资源列表 */ public function get_resources() { if (!check_ajax_referer('mrb_frontend_nonce', 'nonce', false)) { wp_die('安全验证失败', 403); } global $wpdb; $table_name = $wpdb->prefix . 'resources'; $resources = $wpdb->get_results( "SELECT * FROM $table_name WHERE status = 1 ORDER BY type, name ASC" ); $formatted_resources = array(); foreach ($resources as $resource) { $formatted_resources[] = array( 'id' => $resource->id, 'name' => $resource->name, 'type' => $resource->type, 'description' => $resource->description, 'quantity' => $resource->quantity, 'available' => $resource->quantity // 简化处理,实际需要计算已预订数量 ); } wp_send_json_success($formatted_resources); } /** * 获取预订数据 */ public function get_bookings() { if (!check_ajax_referer('mrb_frontend_nonce', 'nonce', false)) { wp_die('安全验证失败', 403); } global $wpdb; $start = isset($_POST['start']) ? sanitize_text_field($_POST['start']) : date('Y-m-d'); $end = isset($_POST['end']) ? sanitize_text_field($_POST['end']) : date('Y-m-d', strtotime('+1 month')); $room_id = isset($_POST['room_id']) ? intval($_POST['room_id']) : 0; $bookings_table = $wpdb->prefix . 'bookings'; $rooms_table = $wpdb->prefix . 'meeting_rooms'; // 构建查询 $where = array("b.start_time >= %s", "b.end_time <= %s", "b.status != 'cancelled'"); $params = array($start, $end); if ($room_id > 0) { $where[] = "b.room_id = %d"; $params[] = $room_id; } $where_sql = implode(' AND ', $where); $query = $wpdb->prepare( "SELECT b.*, r.name as room_name, r.color as room_color FROM $bookings_table b LEFT JOIN $rooms_table r ON b.room_id = r.id WHERE $where_sql ORDER BY b.start_time ASC", $params ); $bookings = $wpdb->get_results($query); // 格式化FullCalendar事件数据 $events = array(); $current_user_id = get_current_user_id(); foreach ($bookings as $booking) { $color = '#3788d8'; // 默认蓝色 if ($booking->user_id == $current_user_id) { $color = '#28a745'; // 用户自己的预订,绿色 } elseif ($booking->status == 'pending') { $color = '#ffc107'; // 待审核,黄色 } $events[] = array( 'id' => $booking->id, 'title' => $booking->title . ' (' . $booking->room_name . ')', 'start' => $booking->start_time, 'end' => $booking->end_time, 'color' => $color, 'extendedProps' => array( 'room_id' => $booking->room_id, 'room_name' => $booking->room_name, 'description' => $booking->description, 'attendees' => $booking->attendees, 'status' => $booking->status, 'user_id' => $booking->user_id ) ); } wp_send_json_success($events); } /** * 创建新预订 */ public function create_booking() { if (!check_ajax_referer('mrb_frontend_nonce', 'nonce', false)) { wp_die('安全验证失败', 403); } // 验证用户登录 if (!is_user_logged_in()) { wp_send_json_error('请先登录'); } // 验证输入数据 $required_fields = array('room_id', 'title', 'start_time', 'end_time', 'attendees'); foreach ($required_fields as $field) { if (empty($_POST[$field])) { wp_send_json_error('缺少必要字段: ' . $field); } } global $wpdb; $room_id = intval($_POST['room_id']); $user_id = get_current_user_id(); $title = sanitize_text_field($_POST['title']); $description = isset($_POST['description']) ? sanitize_textarea_field($_POST['description']) : ''; $start_time = sanitize_text_field($_POST['start_time']); $end_time = sanitize_text_field($_POST['end_time']); $attendees = intval($_POST['attendees']); $resources = isset($_POST['resources']) ? $_POST['resources'] : array(); // 检查会议室是否存在且可用 $room = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}meeting_rooms WHERE id = %d AND status = 1", $room_id )); if (!$room) { wp_send_json_error('会议室不存在或不可用'); } // 检查容量 if ($attendees > $room->capacity) { wp_send_json_error('参会人数超过会议室容量'); } // 检查时间冲突 $conflict = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}bookings WHERE room_id = %d AND status != 'cancelled' AND ( (start_time < %s AND end_time > %s) OR (start_time >= %s AND start_time < %s) OR (end_time > %s AND end_time <= %s) )", $room_id, $end_time, $start_time, $start_time, $end_time, $start_time, $end_time )); if ($conflict > 0) { wp_send_json_error('该时间段已被预订'); } // 检查资源可用性 $resource_errors = array(); if (is_array($resources) && !empty($resources)) { foreach ($resources as $resource_id => $quantity) { $resource_id = intval($resource_id); $quantity = intval($quantity); if ($quantity > 0) { // 检查资源是否存在 $resource = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}resources WHERE id = %d AND status = 1", $resource_id )); if (!$resource) { $resource_errors[] = "资源ID {$resource_id} 不存在"; continue

发表评论

一步步教你,集成在线简易海报设计与宣传图生成工具到网站

一步步教你,集成在线简易海报设计与宣传图生成工具到WordPress网站 引言:为什么网站需要集成海报设计工具? 在当今数字化营销时代,视觉内容的重要性不言而喻。据统计,带有图片的社交媒体帖子比纯文本帖子获得更多的互动——平均高出2.3倍的参与度。对于网站运营者而言,为用户提供便捷的视觉内容创建工具,不仅能增强用户粘性,还能促进内容生成和分享。 然而,专业的图像设计软件如Photoshop学习成本高,而在线设计工具往往需要跳转到第三方网站,导致用户流失。将简易海报设计与宣传图生成工具直接集成到WordPress网站中,可以完美解决这一问题。用户无需离开您的网站即可创建吸引人的视觉内容,这不仅能提升用户体验,还能增加网站的功能价值和竞争力。 本文将详细指导您如何通过WordPress代码二次开发,将在线简易海报设计与宣传图生成工具集成到您的网站中,实现这一实用的互联网小工具功能。 第一部分:准备工作与环境配置 1.1 工具集成方案选择 在开始之前,我们需要确定合适的技术方案。目前主要有三种集成方式: 使用现有插件:如Canva的嵌入代码或类似设计工具的API 自定义开发简单设计器:基于HTML5 Canvas和JavaScript 混合方案:结合现有库和自定义功能 考虑到灵活性和控制度,我们将选择第二种方案,使用Fabric.js库构建一个轻量级但功能齐全的设计工具。Fabric.js是一个强大的Canvas库,专门用于处理图形和图像操作。 1.2 开发环境准备 首先,确保您具备以下条件: 一个运行中的WordPress网站(版本5.0以上) 访问WordPress文件系统的权限(通过FTP或文件管理器) 基础的HTML、CSS、JavaScript和PHP知识 代码编辑器(如VS Code、Sublime Text等) 1.3 创建开发子主题 为了避免主题更新导致自定义代码丢失,我们将在子主题中进行开发: 在WordPress的wp-content/themes/目录下创建新文件夹,命名为my-design-tool-theme 在该文件夹中创建style.css文件,添加以下内容: /* Theme Name: My Design Tool Theme Template: your-parent-theme-folder-name Version: 1.0 Description: 子主题,用于集成海报设计工具 */ 创建functions.php文件,暂时留空 在WordPress后台启用这个子主题 第二部分:构建海报设计器前端界面 2.1 引入必要的JavaScript库 在子主题的functions.php中添加以下代码,引入Fabric.js和其他必要的库: function design_tool_enqueue_scripts() { // 引入Fabric.js库 wp_enqueue_script('fabric-js', 'https://cdnjs.cloudflare.com/ajax/libs/fabric.js/4.5.0/fabric.min.js', array(), '4.5.0', true); // 引入html2canvas用于导出图片 wp_enqueue_script('html2canvas', 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js', array(), '1.4.1', true); // 引入我们的自定义脚本 wp_enqueue_script('design-tool-script', get_stylesheet_directory_uri() . '/js/design-tool.js', array('fabric-js', 'jquery'), '1.0', true); // 引入样式文件 wp_enqueue_style('design-tool-style', get_stylesheet_directory_uri() . '/css/design-tool.css', array(), '1.0'); // 本地化脚本,传递PHP变量到JavaScript wp_localize_script('design-tool-script', 'designToolAjax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('design_tool_nonce') )); } add_action('wp_enqueue_scripts', 'design_tool_enqueue_scripts'); 2.2 创建设计器HTML结构 在子主题目录下创建template-parts文件夹,然后创建design-tool-template.php文件: <?php /** * 海报设计工具模板 */ ?> <div class="design-tool-container"> <div class="design-tool-header"> <h1>简易海报设计工具</h1> <p>创建吸引人的宣传图、社交媒体图片和海报</p> </div> <div class="design-tool-wrapper"> <!-- 左侧工具栏 --> <div class="design-sidebar"> <div class="tool-section"> <h3>模板</h3> <div class="template-thumbnails"> <div class="template-item" data-template="social-media"> <img src="<?php echo get_stylesheet_directory_uri(); ?>/images/template-social.jpg" alt="社交媒体模板"> <span>社交媒体</span> </div> <div class="template-item" data-template="poster"> <img src="<?php echo get_stylesheet_directory_uri(); ?>/images/template-poster.jpg" alt="海报模板"> <span>海报</span> </div> <div class="template-item" data-template="banner"> <img src="<?php echo get_stylesheet_directory_uri(); ?>/images/template-banner.jpg" alt="横幅模板"> <span>横幅广告</span> </div> </div> </div> <div class="tool-section"> <h3>元素</h3> <button class="tool-btn" data-action="add-text">添加文本</button> <button class="tool-btn" data-action="add-image">添加图片</button> <button class="tool-btn" data-action="add-shape">添加形状</button> <button class="tool-btn" data-action="add-background">更改背景</button> </div> <div class="tool-section"> <h3>属性</h3> <div id="property-panel"> <p>选择元素以编辑属性</p> </div> </div> </div> <!-- 中间画布区域 --> <div class="design-main"> <div class="canvas-toolbar"> <button id="btn-undo" title="撤销">↶</button> <button id="btn-redo" title="重做">↷</button> <button id="btn-clear" title="清空画布">×</button> <select id="canvas-size"> <option value="800x600">800×600 (通用)</option> <option value="1200x628">1200×628 (Facebook)</option> <option value="1080x1080">1080×1080 (Instagram)</option> <option value="1500x500">1500×500 (Twitter横幅)</option> </select> </div> <div class="canvas-container"> <canvas id="design-canvas"></canvas> </div> <div class="canvas-footer"> <button id="btn-save-draft" class="btn-secondary">保存草稿</button> <button id="btn-export-png" class="btn-primary">导出为PNG</button> <button id="btn-export-jpg" class="btn-primary">导出为JPG</button> </div> </div> <!-- 右侧资源库 --> <div class="design-resources"> <div class="tool-section"> <h3>背景图片</h3> <div class="background-thumbnails"> <?php for($i=1; $i<=6; $i++): ?> <div class="bg-item" data-bg="<?php echo get_stylesheet_directory_uri(); ?>/images/bg<?php echo $i; ?>.jpg"> <img src="<?php echo get_stylesheet_directory_uri(); ?>/images/bg<?php echo $i; ?>.jpg" alt="背景<?php echo $i; ?>"> </div> <?php endfor; ?> </div> </div> <div class="tool-section"> <h3>字体选择</h3> <select id="font-selector"> <option value="Arial">Arial</option> <option value="Helvetica">Helvetica</option> <option value="Times New Roman">Times New Roman</option> <option value="Georgia">Georgia</option> <option value="Courier New">Courier New</option> </select> </div> <div class="tool-section"> <h3>颜色选择</h3> <div class="color-palette"> <div class="color-item" data-color="#FF0000" style="background-color:#FF0000;"></div> <div class="color-item" data-color="#00FF00" style="background-color:#00FF00;"></div> <div class="color-item" data-color="#0000FF" style="background-color:#0000FF;"></div> <div class="color-item" data-color="#FFFF00" style="background-color:#FFFF00;"></div> <div class="color-item" data-color="#FF00FF" style="background-color:#FF00FF;"></div> <div class="color-item" data-color="#00FFFF" style="background-color:#00FFFF;"></div> <div class="color-item" data-color="#000000" style="background-color:#000000;"></div> <div class="color-item" data-color="#FFFFFF" style="background-color:#FFFFFF; border:1px solid #ccc;"></div> </div> <input type="color" id="color-picker" value="#FF0000"> </div> </div> </div> <!-- 模态框 --> <div id="image-upload-modal" class="modal"> <div class="modal-content"> <span class="close-modal">&times;</span> <h3>上传图片</h3> <div class="upload-options"> <div class="upload-option"> <h4>从电脑上传</h4> <input type="file" id="file-upload" accept="image/*"> <button id="btn-upload">上传</button> </div> <div class="upload-option"> <h4>从URL添加</h4> <input type="text" id="image-url" placeholder="输入图片URL"> <button id="btn-add-url">添加</button> </div> </div> </div> </div> </div> 2.3 设计器CSS样式 创建css/design-tool.css文件: /* 设计工具主容器 */ .design-tool-container { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; max-width: 1400px; margin: 0 auto; padding: 20px; background-color: #f5f7fa; min-height: 100vh; } .design-tool-header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid #e1e5eb; } .design-tool-header h1 { color: #2c3e50; margin-bottom: 10px; } .design-tool-header p { color: #7f8c8d; font-size: 1.1rem; } /* 设计工具布局 */ .design-tool-wrapper { display: flex; gap: 20px; background-color: white; border-radius: 10px; box-shadow: 0 5px 15px rgba(0,0,0,0.08); overflow: hidden; min-height: 700px; } .design-sidebar, .design-resources { width: 250px; padding: 20px; background-color: #f8f9fa; border-right: 1px solid #e9ecef; } .design-resources { border-right: none; border-left: 1px solid #e9ecef; } .design-main { flex: 1; display: flex; flex-direction: column; padding: 20px; } /* 工具栏样式 */ .tool-section { margin-bottom: 30px; } .tool-section h3 { color: #495057; font-size: 1.1rem; margin-bottom: 15px; padding-bottom: 8px; border-bottom: 1px solid #dee2e6; } .tool-btn { display: block; width: 100%; padding: 10px 15px; margin-bottom: 10px; background-color: #4dabf7; color: white; border: none; border-radius: 5px; cursor: pointer; transition: background-color 0.2s; font-size: 0.95rem; } .tool-btn:hover { background-color: #339af0; } /* 模板缩略图 */ .template-thumbnails { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } .template-item { border: 1px solid #dee2e6; border-radius: 5px; overflow: hidden; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; } .template-item:hover { transform: translateY(-3px); box-shadow: 0 5px 10px rgba(0,0,0,0.1); } .template-item img { width: 100%; height: 70px; object-fit: cover; } .template-item span { display: block; text-align: center; padding: 5px; font-size: 0.8rem; color: #495057; } /* 画布区域 */ .canvas-toolbar { display: flex; gap: 10px; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #e9ecef; } .canvas-toolbar button, .canvas-toolbar select { padding: 8px 15px; border: 1px solid #ced4da; border-radius: 5px; background-color: white; cursor: pointer; } .canvas-toolbar button:hover { background-color: #f8f9fa; } .canvas-container { flex: 1; display: flex; justify-content: center; align-items: center; border: 1px dashed #adb5bd; border-radius: 5px; background-color: #f8f9fa; margin-bottom: 20px; overflow: auto; } #design-canvas { background-color: white; box-shadow: 0 3px 10px rgba(0,0,0,0.1); } .canvas-footer { display: flex; justify-content: center; gap: 15px; padding-top: 20px; border-top: 1px solid #e9ecef; } .btn-primary, .btn-secondary { padding: 12px 25px; border: none; border-radius: 5px; cursor: pointer; font-size: 1rem; font-weight: 500; transition: all 0.2s; } .btn-primary { background-color: #40c057; color: white; } .btn-primary:hover { background-color: #37b24d; } .btn-secondary { background-color: #e9ecef; color: #495057; } .btn-secondary:hover { background-color: #dee2e6; } /* 资源库样式 */ .background-thumbnails { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } .bg-item { border: 1px solid #dee2e6; border-radius: 5px; overflow: hidden; cursor: pointer; height: 70px; } .bg-item img { width: 100%; height: 100%; object-fit: cover; } .color-palette { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 15px; } .color-item { width: 30px; height: 30px; border-radius: 50%; cursor: pointer; border: 2px solid transparent; } .color-item:hover { border-color: #495057; } #color-picker { width: 100%; height: 40px; border: none; cursor: pointer; border-radius: 5px; overflow: hidden; } #font-selector { width: 100%; padding: 8px; border: 1px solid #ced4da; border-radius: 5px; } /* 模态框样式 */ .modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); } .modal-content { background-color: white; margin: 10% auto; padding: 25px; width: 500px; border-radius: 10px; box-shadow: 0 5px 20px rgba(0,0,0,0.2); position: relative; } .close-modal { position: absolute; right: 20px; top: 15px; font-size: 28px; cursor: pointer; color: #7f8c8d; } .close-modal:hover { color: #2c3e50; } .upload-options { display: flex; gap: 20px; margin-top: 20px; } .upload-option { flex: 1; padding: 20px; border: 1px solid #e9ecef; border-radius: 5px; } .upload-option h4 { margin-top: 0; margin-bottom: 15px; color: #495057; } .upload-option input { width: 100%; padding: 8px; margin-bottom: 10px; border: 1px solid #ced4da; border-radius: 5px; } .upload-option button { padding: 8px 15px; background-color: #4dabf7; color: white; border: none; border-radius: 5px; cursor: pointer; } / 响应式设计 /@media (max-width: 1200px) { .design-tool-wrapper { flex-direction: column; } .design-sidebar, .design-resources { width: 100%; border-right: none; border-left: none; border-bottom: 1px solid #e9ecef; } .design-resources { border-top: 1px solid #e9ecef; } } / 属性面板样式 / property-panel { background-color: white; padding: 15px; border-radius: 5px; border: 1px solid #e9ecef; min-height: 150px; } .property-group { margin-bottom: 15px; } .property-group label { display: block; margin-bottom: 5px; font-size: 0.9rem; color: #495057; } .property-group input,.property-group select { width: 100%; padding: 6px; border: 1px solid #ced4da; border-radius: 3px; } ## 第三部分:实现设计器核心功能 ### 3.1 初始化设计器JavaScript 创建`js/design-tool.js`文件,开始实现核心功能: jQuery(document).ready(function($) { // 初始化变量 let canvas, currentTool = 'select', history = [], historyIndex = -1; let selectedObject = null; // 初始化画布 function initCanvas(width = 800, height = 600) { canvas = new fabric.Canvas('design-canvas', { width: width, height: height, backgroundColor: '#ffffff', preserveObjectStacking: true }); // 添加默认文本 addDefaultText(); // 监听选择变化 canvas.on('selection:created', handleSelection); canvas.on('selection:updated', handleSelection); canvas.on('selection:cleared', clearSelection); // 监听对象修改 canvas.on('object:modified', saveState); canvas.on('object:added', saveState); canvas.on('object:removed', saveState); // 保存初始状态 saveState(); } // 添加默认文本 function addDefaultText() { const text = new fabric.Textbox('双击编辑文本', { left: 100, top: 100, width: 200, fontSize: 24, fill: '#333333', fontFamily: 'Arial' }); canvas.add(text); canvas.setActiveObject(text); } // 处理选择对象 function handleSelection(options) { selectedObject = canvas.getActiveObject(); updatePropertyPanel(); } // 清除选择 function clearSelection() { selectedObject = null; updatePropertyPanel(); } // 更新属性面板 function updatePropertyPanel() { const panel = $('#property-panel'); if (!selectedObject) { panel.html('<p>选择元素以编辑属性</p>'); return; } let html = '<div class="property-group">'; html += '<label>元素类型: ' + selectedObject.type + '</label>'; html += '</div>'; // 通用属性 html += '<div class="property-group">'; html += '<label>位置 X:</label>'; html += '<input type="number" id="prop-left" value="' + Math.round(selectedObject.left) + '">'; html += '</div>'; html += '<div class="property-group">'; html += '<label>位置 Y:</label>'; html += '<input type="number" id="prop-top" value="' + Math.round(selectedObject.top) + '">'; html += '</div>'; // 文本特定属性 if (selectedObject.type === 'textbox' || selectedObject.type === 'text') { html += '<div class="property-group">'; html += '<label>文本内容:</label>'; html += '<input type="text" id="prop-text" value="' + selectedObject.text + '">'; html += '</div>'; html += '<div class="property-group">'; html += '<label>字体大小:</label>'; html += '<input type="number" id="prop-font-size" value="' + selectedObject.fontSize + '">'; html += '</div>'; html += '<div class="property-group">'; html += '<label>字体:</label>'; html += '<select id="prop-font-family">'; html += '<option value="Arial"' + (selectedObject.fontFamily === 'Arial' ? ' selected' : '') + '>Arial</option>'; html += '<option value="Helvetica"' + (selectedObject.fontFamily === 'Helvetica' ? ' selected' : '') + '>Helvetica</option>'; html += '<option value="Times New Roman"' + (selectedObject.fontFamily === 'Times New Roman' ? ' selected' : '') + '>Times New Roman</option>'; html += '<option value="Georgia"' + (selectedObject.fontFamily === 'Georgia' ? ' selected' : '') + '>Georgia</option>'; html += '</select>'; html += '</div>'; html += '<div class="property-group">'; html += '<label>文本颜色:</label>'; html += '<input type="color" id="prop-fill" value="' + rgbToHex(selectedObject.fill) + '">'; html += '</div>'; } // 图像特定属性 if (selectedObject.type === 'image') { html += '<div class="property-group">'; html += '<label>透明度:</label>'; html += '<input type="range" id="prop-opacity" min="0" max="1" step="0.1" value="' + selectedObject.opacity + '">'; html += '</div>'; } panel.html(html); // 绑定属性变化事件 bindPropertyEvents(); } // 绑定属性变化事件 function bindPropertyEvents() { if (!selectedObject) return; // 位置属性 $('#prop-left').on('change', function() { selectedObject.set('left', parseInt($(this).val())); canvas.renderAll(); saveState(); }); $('#prop-top').on('change', function() { selectedObject.set('top', parseInt($(this).val())); canvas.renderAll(); saveState(); }); // 文本属性 $('#prop-text').on('change', function() { if (selectedObject.type === 'textbox' || selectedObject.type === 'text') { selectedObject.set('text', $(this).val()); canvas.renderAll(); saveState(); } }); $('#prop-font-size').on('change', function() { if (selectedObject.type === 'textbox' || selectedObject.type === 'text') { selectedObject.set('fontSize', parseInt($(this).val())); canvas.renderAll(); saveState(); } }); $('#prop-font-family').on('change', function() { if (selectedObject.type === 'textbox' || selectedObject.type === 'text') { selectedObject.set('fontFamily', $(this).val()); canvas.renderAll(); saveState(); } }); $('#prop-fill').on('change', function() { if (selectedObject.type === 'textbox' || selectedObject.type === 'text') { selectedObject.set('fill', $(this).val()); canvas.renderAll(); saveState(); } }); // 图像透明度 $('#prop-opacity').on('input', function() { if (selectedObject.type === 'image') { selectedObject.set('opacity', parseFloat($(this).val())); canvas.renderAll(); saveState(); } }); } // RGB转十六进制颜色 function rgbToHex(rgb) { if (!rgb) return '#000000'; if (rgb.startsWith('#')) return rgb; if (rgb.startsWith('rgb')) { const values = rgb.match(/d+/g); if (values && values.length >= 3) { const r = parseInt(values[0]).toString(16).padStart(2, '0'); const g = parseInt(values[1]).toString(16).padStart(2, '0'); const b = parseInt(values[2]).toString(16).padStart(2, '0'); return `#${r}${g}${b}`; } } return '#000000'; } // 保存状态到历史记录 function saveState() { // 只保留最近20个状态 if (history.length > 20) { history.shift(); } // 如果当前不是最新状态,移除后面的状态 if (historyIndex < history.length - 1) { history = history.slice(0, historyIndex + 1); } const state = JSON.stringify(canvas.toJSON()); history.push(state); historyIndex = history.length - 1; } // 撤销 function undo() { if (historyIndex > 0) { historyIndex--; loadState(history[historyIndex]); } } // 重做 function redo() { if (historyIndex < history.length - 1) { historyIndex++; loadState(history[historyIndex]); } } // 加载状态 function loadState(state) { canvas.loadFromJSON(state, function() { canvas.renderAll(); // 重新绑定事件 canvas.getObjects().forEach(function(obj) { obj.setCoords(); }); }); } // 添加文本 function addText() { const text = new fabric.Textbox('新文本', { left: 100, top: 100, width: 200, fontSize: 24, fill: $('#color-picker').val(), fontFamily: $('#font-selector').val() }); canvas.add(text); canvas.setActiveObject(text); updatePropertyPanel(); } // 添加形状 function addShape(shapeType = 'rect') { let shape; const color = $('#color-picker').val(); switch(shapeType) { case 'rect': shape = new fabric.Rect({ left: 150, top: 150, width: 100, height: 100, fill: color, strokeWidth: 2, stroke: '#333333' }); break; case 'circle': shape = new fabric.Circle({ left: 150, top: 150, radius: 50, fill: color, strokeWidth: 2, stroke: '#333333' }); break; case 'triangle': shape = new fabric.Triangle({ left: 150, top: 150, width: 100, height: 100, fill: color, strokeWidth: 2, stroke: '#333333' }); break; } canvas.add(shape); canvas.setActiveObject(shape); updatePropertyPanel(); } // 添加图片 function addImage(url) { fabric.Image.fromURL(url, function(img) { // 限制图片大小 const maxWidth = 400; const maxHeight = 400; let scale = 1; if (img.width > maxWidth) { scale = maxWidth / img.width; } if (img.height * scale > maxHeight) { scale = maxHeight / img.height; } img.scale(scale); img.set({ left: 200, top: 200 }); canvas.add(img); canvas.setActiveObject(img); updatePropertyPanel(); }); } // 更改背景 function changeBackground(type, value) { if (type === 'color') { canvas.setBackgroundColor(value, canvas.renderAll.bind(canvas)); } else if (type === 'image') { fabric.Image.fromURL(value, function(img) { canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas), { scaleX: canvas.width / img.width, scaleY: canvas.height / img.height }); }); } saveState(); } // 更改画布尺寸 function changeCanvasSize(width, height) { canvas.setDimensions({width: width, height: height}); canvas.renderAll(); saveState(); } // 导出为图片 function exportImage(format = 'png', quality = 1.0) { const dataURL = canvas.toDataURL({ format: format, quality: quality }); // 创建下载链接 const link = document.createElement('a'); link.download = 'design-' + Date.now() + '.' + format; link.href = dataURL; document.body.appendChild(link); link.click(); document.body.removeChild(link); } // 保存草稿到服务器 function saveDraft() { const designData = JSON.stringify(canvas.toJSON()); $.ajax({ url: designToolAjax.ajax_url, type: 'POST', data: { action: 'save_design_draft', nonce: designToolAjax.nonce, design_data: designData }, success: function(response) { if (response.success) { alert('草稿保存成功!'); } else { alert('保存失败:' + response.data); } }, error: function() { alert('网络错误,请重试'); } }); } // 初始化事件监听 function initEventListeners() { // 工具栏按钮 $('[data-action="add-text"]').click(addText); $('[data-action="add-shape"]').click(function() { addShape('rect'); }); $('[data-action="add-image"]').click(function() { $('#image-upload-modal').show(); }); $('[data-action="add-background"]').click(function() { changeBackground('color', $('#color-picker').val()); }); // 模板选择 $('.template-item').click(function() { const template = $(this).data('template'); loadTemplate(template); }); // 背景选择 $('.bg-item').click(function() { const bgUrl = $(this).data('bg'); changeBackground('image', bgUrl); }); // 颜色选择 $('.color-item').click(function() { const color = $(this).data('color'); $('#color-picker').val(color); // 如果当前有选中对象,应用颜色 if (selectedObject && selectedObject.type === 'textbox') { selectedObject.set('fill', color); canvas.renderAll(); saveState(); updatePropertyPanel(); } }); $('#color-picker').change(function() { const color = $(this).val(); // 如果当前有选中文本对象,应用颜色 if (selectedObject && selectedObject.type === 'textbox') { selectedObject.set('fill', color); canvas.renderAll(); saveState(); updatePropertyPanel(); } }); // 字体选择 $('#font-selector').change(function() { const font = $(this).val(); // 如果当前有选中文本对象,应用字体 if (selectedObject && selectedObject.type === 'textbox') { selectedObject.set('fontFamily', font); canvas.renderAll(); saveState(); updatePropertyPanel(); } }); // 画布尺寸选择 $('#canvas-size').change(function() { const size = $(this).val().split('x'); const width = parseInt(size[0]); const height = parseInt(size[1]); changeCanvasSize(width, height); }); // 工具栏按钮 $('#btn-undo').click(undo); $('#btn-redo').click(redo); $('#btn-clear').click(function() { if (confirm('确定要清空画布吗?')) { canvas.clear(); addDefaultText(); saveState(); } }); // 导出按钮 $('#btn-export-png').click(function() { exportImage('png'); }); $('#btn-export-jpg').click(function() { exportImage('jpeg', 0.9); }); // 保存草稿 $('#btn-save-draft').click(saveDraft); // 图片上传模态框 $('.close-modal').click(function() { $('#image-upload-modal').hide(); }); $(window).click(function(event) { if ($(event.target).is('#image-upload-modal')) { $('#image-upload-modal').hide(); } }); // 上传图片 $('#btn-upload').click(function() { const fileInput = $('#file-upload')[0]; if (fileInput.files.length === 0) { alert('请选择图片文件'); return; } const file = fileInput.files[0]; const reader = new FileReader(); reader.onload = function(e) { addImage(e.target.result); $('#image-upload-modal').hide(); $('#file-upload').val(''); }; reader.readAsDataURL(file); }); // 从URL添加图片

发表评论