为 WooCommerce 商店添加了一个全面的库存通知系统,让客户可以注册补货提醒。当产品补货时,订阅的客户会自动收到定制的电子邮件通知。
### 主要功能
* **自动通知** – 产品补货时自动发送电子邮件通知
* **降价警报** – 产品降价时可选通知
* **可变产品支持** – 适用于简单产品和所有产品变体
* **可自定义电子邮件** – 完全自定义通知电子邮件模板
* **我的帐户集成** – 客户可以从他们的帐户管理他们的订阅
* **详细统计数据** – 跟踪您最想要的缺货产品
* **AJAX 订阅表单** – 流畅的用户体验,无需重新加载页面
* **短代码支持** – 使用简单的短代码在任何地方添加通知表单
* **可自定义样式** – 将表单与您的主题设计相匹配
* **GDPR 就绪** – 客户可以管理和删除他们的订阅
相关文章: WooCommerce自定义订单状态
<?php /** * Plugin Name: BDFG Stock Notifier for WooCommerce * Plugin URI: https://beiduofengou.net/2025/01/22/stock-notifier-for-woocommerce/ * Description: Adds a powerful notification system to your WooCommerce store that lets customers sign up for back-in-stock alerts. When products return to stock, subscribed customers automatically receive customized email notifications. * Version: 2.4.0 * Author: beiduofengou * Author URI: https://beiduofengou.net * Text Domain: bdfg-stock-notifier * Domain Path: /languages * Requires at least: 5.6 * Requires PHP: 7.4 * WC requires at least: 5.0 * WC tested up to: 8.5 * License: GPL v2 or later * License URI: https://www.gnu.org/licenses/gpl-2.0.html */ // Prevent direct file access if (!defined('ABSPATH')) { exit; } // Define plugin constants define('BDFG_STOCK_NOTIFIER_VERSION', '2.4.0'); define('BDFG_STOCK_NOTIFIER_FILE', __FILE__); define('BDFG_STOCK_NOTIFIER_PATH', plugin_dir_path(__FILE__)); define('BDFG_STOCK_NOTIFIER_URL', plugin_dir_url(__FILE__)); define('BDFG_STOCK_NOTIFIER_BASENAME', plugin_basename(__FILE__)); // Load text domain function bdfg_stock_notifier_load_textdomain() { load_plugin_textdomain('bdfg-stock-notifier', false, dirname(plugin_basename(__FILE__)) . '/languages/'); } add_action('plugins_loaded', 'bdfg_stock_notifier_load_textdomain'); // Check if WooCommerce is installed function bdfg_stock_notifier_check_woocommerce() { if (!class_exists('WooCommerce')) { add_action('admin_notices', 'bdfg_stock_notifier_woocommerce_missing_notice'); return false; } return true; } // WooCommerce missing notice function bdfg_stock_notifier_woocommerce_missing_notice() { ?> <div class="notice notice-error"> <p><?php _e('BDFG Stock Notifier requires WooCommerce to be installed and active.', 'bdfg-stock-notifier'); ?></p> </div> <?php } /** * Create database tables for stock notifications */ function bdfg_stock_notifier_create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'bdfg_stock_subscriptions'; $sql = "CREATE TABLE $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, email varchar(100) NOT NULL, product_id bigint(20) NOT NULL, variation_id bigint(20) DEFAULT 0, date_created datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, status varchar(20) DEFAULT 'pending' NOT NULL, user_id bigint(20) DEFAULT 0, notify_price_drop tinyint(1) DEFAULT 0, PRIMARY KEY (id), KEY email (email), KEY product_id (product_id), KEY variation_id (variation_id), KEY user_id (user_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } // Plugin activation hook register_activation_hook(__FILE__, 'bdfg_stock_notifier_activate'); function bdfg_stock_notifier_activate() { if (bdfg_stock_notifier_check_woocommerce()) { bdfg_stock_notifier_create_tables(); // Set default options $default_options = array( 'enable_stock_notifications' => 'yes', 'enable_price_drop' => 'no', 'notification_email_subject' => __('Product Back in Stock - {product_name}', 'bdfg-stock-notifier'), 'notification_email_template' => __("Hello,\n\nGood news! The product you were interested in is back in stock:\n\n{product_name}\n\nYou can purchase it now at: {product_url}\n\nBest regards,\n{site_name}", 'bdfg-stock-notifier'), 'display_position' => 'woocommerce_single_product_summary', 'display_priority' => 30, 'button_color' => '#0066cc', 'button_text_color' => '#ffffff', ); add_option('bdfg_stock_notifier_settings', $default_options); // Create logs directory $upload_dir = wp_upload_dir(); $logs_dir = trailingslashit($upload_dir['basedir']) . 'bdfg-stock-notifier-logs'; if (!file_exists($logs_dir)) { wp_mkdir_p($logs_dir); // Add an index.php file to prevent directory listing @file_put_contents($logs_dir . '/index.php', "<?php\n// Silence is golden."); } } } // Plugin uninstall hook register_uninstall_hook(__FILE__, 'bdfg_stock_notifier_uninstall'); function bdfg_stock_notifier_uninstall() { global $wpdb; // Let's delete the database table $table_name = $wpdb->prefix . 'bdfg_stock_subscriptions'; $wpdb->query("DROP TABLE IF EXISTS $table_name"); // Delete plugin options delete_option('bdfg_stock_notifier_settings'); // Clean up logs $upload_dir = wp_upload_dir(); $logs_dir = trailingslashit($upload_dir['basedir']) . 'bdfg-stock-notifier-logs'; if (file_exists($logs_dir)) { foreach (glob($logs_dir . '/*.*') as $file) { @unlink($file); } @rmdir($logs_dir); } } // Core plugin class class BDFG_Stock_Notifier { private static $instance = null; private $settings; public static function instance() { if (is_null(self::$instance)) { self::$instance = new self(); } return self::$instance; } private function __construct() { if (!bdfg_stock_notifier_check_woocommerce()) { return; } $this->settings = get_option('bdfg_stock_notifier_settings', array()); // Initialize hooks add_action('init', array($this, 'init_hooks')); // Admin settings if (is_admin()) { add_action('admin_menu', array($this, 'add_admin_menu')); add_action('admin_init', array($this, 'register_settings')); add_filter('plugin_action_links_' . BDFG_STOCK_NOTIFIER_BASENAME, array($this, 'add_plugin_action_links')); } } public function init_hooks() { // Frontend hooks add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts')); // Add notification form based on admin settings $position = isset($this->settings['display_position']) ? $this->settings['display_position'] : 'woocommerce_single_product_summary'; $priority = isset($this->settings['display_priority']) ? intval($this->settings['display_priority']) : 30; add_action($position, array($this, 'display_stock_notification_form'), $priority); // AJAX handlers add_action('wp_ajax_bdfg_stock_notifier_subscribe', array($this, 'handle_subscription_ajax')); add_action('wp_ajax_nopriv_bdfg_stock_notifier_subscribe', array($this, 'handle_subscription_ajax')); // Product stock status change notifications add_action('woocommerce_product_set_stock_status', array($this, 'product_stock_status_changed'), 10, 3); add_action('woocommerce_variation_set_stock_status', array($this, 'variation_stock_status_changed'), 10, 3); // Price change tracking add_action('woocommerce_product_set_price', array($this, 'track_price_change'), 10, 2); add_action('woocommerce_variation_set_price', array($this, 'track_variation_price_change'), 10, 2); // Add shortcode add_shortcode('bdfg_stock_notifier', array($this, 'shortcode_handler')); // User account integration add_action('woocommerce_account_stock-notifications_endpoint', array($this, 'account_stock_notifications_content')); add_filter('woocommerce_account_menu_items', array($this, 'add_account_menu_item'), 40); add_filter('woocommerce_get_query_vars', array($this, 'add_account_endpoint')); // Register custom REST API endpoints add_action('rest_api_init', array($this, 'register_rest_endpoints')); } /** * Register and load frontend assets */ public function enqueue_scripts() { if (is_product() || has_shortcode(get_post_field('post_content', get_the_ID()), 'bdfg_stock_notifier')) { wp_enqueue_style('bdfg-stock-notifier', BDFG_STOCK_NOTIFIER_URL . 'assets/css/frontend.css', array(), BDFG_STOCK_NOTIFIER_VERSION); wp_enqueue_script('bdfg-stock-notifier', BDFG_STOCK_NOTIFIER_URL . 'assets/js/frontend.js', array('jquery'), BDFG_STOCK_NOTIFIER_VERSION, true); // Custom button styling $button_color = isset($this->settings['button_color']) ? sanitize_hex_color($this->settings['button_color']) : '#0066cc'; $button_text_color = isset($this->settings['button_text_color']) ? sanitize_hex_color($this->settings['button_text_color']) : '#ffffff'; $custom_css = " .bdfg-stock-notification-submit { background-color: {$button_color} !important; color: {$button_text_color} !important; } .bdfg-stock-notification-submit:hover { background-color: " . $this->adjust_brightness($button_color, -10) . " !important; } "; wp_add_inline_style('bdfg-stock-notifier', $custom_css); wp_localize_script('bdfg-stock-notifier', 'bdfgStockNotifier', array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('bdfg_stock_notifier_nonce'), 'i18n' => array( 'subscribe' => __('Notify Me When Available', 'bdfg-stock-notifier'), 'subscribed' => __('We\'ll notify you when back in stock', 'bdfg-stock-notifier'), 'select_options' => __('Please select product options first', 'bdfg-stock-notifier'), 'email_invalid' => __('Please enter a valid email address', 'bdfg-stock-notifier'), 'error' => __('Something went wrong. Please try again.', 'bdfg-stock-notifier') ), 'settings' => array( 'enable_price_drop' => isset($this->settings['enable_price_drop']) ? $this->settings['enable_price_drop'] : 'no' ) )); } } /** * Utility function to darken/lighten colors */ private function adjust_brightness($hex, $steps) { $steps = max(-255, min(255, $steps)); $hex = str_replace('#', '', $hex); if (strlen($hex) == 3) { $hex = str_repeat(substr($hex, 0, 1), 2) . str_repeat(substr($hex, 1, 1), 2) . str_repeat(substr($hex, 2, 1), 2); } $r = hexdec(substr($hex, 0, 2)); $g = hexdec(substr($hex, 2, 2)); $b = hexdec(substr($hex, 4, 2)); $r = max(0, min(255, $r + $steps)); $g = max(0, min(255, $g + $steps)); $b = max(0, min(255, $b + $steps)); return '#' . dechex($r) . dechex($g) . dechex($b); } /** * Display notification form shortcode handler */ public function shortcode_handler($atts) { $atts = shortcode_atts(array( 'product_id' => 0, 'variation_id' => 0, 'button_text' => '', 'title' => '', ), $atts, 'bdfg_stock_notifier'); $product_id = absint($atts['product_id']); // If no product ID specified, use current product if (!$product_id && is_product()) { global $product; $product_id = $product->get_id(); } if (!$product_id) { return '<p>' . __('Product ID is required for the stock notifier form.', 'bdfg-stock-notifier') . '</p>'; } $product = wc_get_product($product_id); if (!$product) { return '<p>' . __('Invalid product specified.', 'bdfg-stock-notifier') . '</p>'; } // Start output buffering ob_start(); $this->display_stock_notification_form($product, $atts); return ob_get_clean(); } /** * Display notification form on product pages */ public function display_stock_notification_form($product = null, $options = array()) { // Check if settings is enabled if (empty($this->settings['enable_stock_notifications']) || $this->settings['enable_stock_notifications'] != 'yes') { return; } // If product is not provided (happens when called from action hook) if (!$product || !is_object($product)) { global $product; } if (!$product || !is_a($product, 'WC_Product')) { return; } $product_id = $product->get_id(); $product_type = $product->get_type(); // Don't show for in-stock products if ($product->is_in_stock() && $product->get_stock_status() != 'outofstock') { return; } // Get options $button_text = isset($options['button_text']) && !empty($options['button_text']) ? sanitize_text_field($options['button_text']) : __('Notify Me When Available', 'bdfg-stock-notifier'); $title = isset($options['title']) && !empty($options['title']) ? sanitize_text_field($options['title']) : __('Out of Stock', 'bdfg-stock-notifier'); $enable_price_drop = isset($this->settings['enable_price_drop']) && $this->settings['enable_price_drop'] == 'yes'; echo '<div class="bdfg-stock-notification-container">'; echo '<h4>' . esc_html($title) . '</h4>'; echo '<p>' . __('This product is currently out of stock. Enter your email to be notified when it becomes available again.', 'bdfg-stock-notifier') . '</p>'; echo '<form class="bdfg-stock-notification-form" data-product-id="' . esc_attr($product_id) . '" data-product-type="' . esc_attr($product_type) . '">'; echo '<input type="email" name="bdfg_email" class="bdfg-stock-notification-email" placeholder="' . esc_attr__('Your Email', 'bdfg-stock-notifier') . '" required>'; // Price drop notification option if ($enable_price_drop) { echo '<div class="bdfg-price-drop-option">'; echo '<label>'; echo '<input type="checkbox" name="bdfg_notify_price_drop" value="1" checked> '; echo esc_html__('Also notify me if this product goes on sale', 'bdfg-stock-notifier'); echo '</label>'; echo '</div>'; } echo '<div class="bdfg-subscription-response"></div>'; echo '<button type="submit" class="bdfg-stock-notification-submit button alt">' . esc_html($button_text) . '</button>'; // Add branding (can be disabled in settings) if (!isset($this->settings['remove_branding']) || $this->settings['remove_branding'] != 'yes') { echo '<div class="bdfg-notifier-branding">'; echo '<small>Powered by <a href="https://beiduofengou.net" target="_blank">BDFG Stock Notifier</a></small>'; echo '</div>'; } echo '</form>'; echo '</div>'; } /** * Handle AJAX subscription requests */ public function handle_subscription_ajax() { check_ajax_referer('bdfg_stock_notifier_nonce', 'nonce'); $email = isset($_POST['email']) ? sanitize_email($_POST['email']) : ''; $product_id = isset($_POST['product_id']) ? absint($_POST['product_id']) : 0; $variation_id = isset($_POST['variation_id']) ? absint($_POST['variation_id']) : 0; $notify_price_drop = isset($_POST['notify_price_drop']) ? (bool)$_POST['notify_price_drop'] : false; if (!is_email($email)) { wp_send_json_error(array('message' => __('Please enter a valid email address.', 'bdfg-stock-notifier'))); exit; } if (!$product_id) { wp_send_json_error(array('message' => __('Invalid product.', 'bdfg-stock-notifier'))); exit; } $product = wc_get_product($product_id); if (!$product) { wp_send_json_error(array('message' => __('Product not found.', 'bdfg-stock-notifier'))); exit; } // For variable products, check if variation is selected if ($product->get_type() == 'variable' && !$variation_id) { wp_send_json_error(array('message' => __('Please select product options before requesting notification.', 'bdfg-stock-notifier'))); exit; } // Only subscribe for out of stock products $check_id = $variation_id ? $variation_id : $product_id; $check_product = wc_get_product($check_id); if ($check_product && $check_product->is_in_stock()) { wp_send_json_error(array( 'message' => __('This product is already in stock.', 'bdfg-stock-notifier'), 'in_stock' => true )); exit; } // Get user ID if logged in $user_id = get_current_user_id(); // Save subscription to database $result = $this->save_subscription($email, $product_id, $variation_id, $user_id, $notify_price_drop); if ($result === true) { // Log the subscription $this->log(sprintf( 'New subscription: Email: %s, Product ID: %d, Variation ID: %d, User ID: %d', $email, $product_id, $variation_id, $user_id )); do_action('bdfg_stock_notifier_after_subscription', $email, $product_id, $variation_id, $user_id); wp_send_json_success(array('message' => __('You will be notified when this product is back in stock.', 'bdfg-stock-notifier'))); } else { wp_send_json_error(array('message' => $result)); } exit; } /** * Save subscription to database */ private function save_subscription($email, $product_id, $variation_id = 0, $user_id = 0, $notify_price_drop = false) { global $wpdb; $table_name = $wpdb->prefix . 'bdfg_stock_subscriptions'; // Check if already subscribed $existing = $wpdb->get_var($wpdb->prepare( "SELECT id FROM $table_name WHERE email = %s AND product_id = %d AND variation_id = %d AND status = 'pending'", $email, $product_id, $variation_id )); if ($existing) { return __('You are already subscribed to this product.', 'bdfg-stock-notifier'); } // Add new subscription $result = $wpdb->insert( $table_name, array( 'email' => $email, 'product_id' => $product_id, 'variation_id' => $variation_id, 'date_created' => current_time('mysql'), 'status' => 'pending', 'user_id' => $user_id, 'notify_price_drop' => $notify_price_drop ? 1 : 0 ), array('%s', '%d', '%d', '%s', '%s', '%d', '%d') ); return $result ? true : __('Failed to save your subscription. Please try again.', 'bdfg-stock-notifier'); } /** * Handle product stock status change */ public function product_stock_status_changed($product_id, $stock_status, $product) { if ($stock_status === 'instock') { $this->send_stock_notifications($product_id, 0); } } /** * Handle variation stock status change */ public function variation_stock_status_changed($variation_id, $stock_status, $product) { if ($stock_status === 'instock') { $parent_id = $product->get_parent_id(); $this->send_stock_notifications($parent_id, $variation_id); } } /** * Track product price changes */ public function track_price_change($product, $price) { if (isset($this->settings['enable_price_drop']) && $this->settings['enable_price_drop'] == 'yes') { $product_id = $product->get_id(); $old_price = $product->get_meta('_bdfg_previous_price', true); if ($old_price && $price < $old_price) { // Price has dropped, notify subscribers $this->send_price_drop_notifications($product_id, 0, $price, $old_price); } // Store current price for future reference $product->update_meta_data('_bdfg_previous_price', $price); $product->save_meta_data(); } } /** * Track variation price changes */ public function track_variation_price_change($variation, $price) { if (isset($this->settings['enable_price_drop']) && $this->settings['enable_price_drop'] == 'yes') { $variation_id = $variation->get_id(); $parent_id = $variation->get_parent_id(); $old_price = $variation->get_meta('_bdfg_previous_price', true); if ($old_price && $price < $old_price) { // Price has dropped, notify subscribers $this->send_price_drop_notifications($parent_id, $variation_id, $price, $old_price); } // Store current price for future reference $variation->update_meta_data('_bdfg_previous_price', $price); $variation->save_meta_data(); } } /** * Send price drop notifications */ private function send_price_drop_notifications($product_id, $variation_id, $new_price, $old_price) { global $wpdb; $table_name = $wpdb->prefix . 'bdfg_stock_subscriptions'; // Get subscribers who opted for price drop notifications if ($variation_id > 0) { $subscriptions = $wpdb->get_results($wpdb->prepare( "SELECT * FROM $table_name WHERE product_id = %d AND variation_id = %d AND notify_price_drop = 1", $product_id, $variation_id )); } else { $subscriptions = $wpdb->get_results($wpdb->prepare( "SELECT * FROM $table_name WHERE product_id = %d AND variation_id = 0 AND notify_price_drop = 1", $product_id )); } if (empty($subscriptions)) { return; } $product = wc_get_product($variation_id > 0 ? $variation_id : $product_id); if (!$product) { return; } $product_name = $product->get_name(); $product_url = get_permalink($product_id); $discount_percent = round((($old_price - $new_price) / $old_price) * 100); // Add variation URL parameter if needed if ($variation_id > 0) { $product_url = add_query_arg('variation_id', $variation_id, $product_url); } $formatted_old_price = wc_price($old_price); $formatted_new_price = wc_price($new_price); // Get email template $subject = __('Price Drop Alert - {product_name} now {discount_percent}% off!', 'bdfg-stock-notifier'); $message = __("Hello,\n\nGood news! The price for {product_name} has dropped!\n\nOld Price: {old_price}\nNew Price: {new_price}\nYou save: {discount_percent}%\n\nGrab this deal now at: {product_url}\n\nBest regards,\n{site_name}", 'bdfg-stock-notifier'); // Replace placeholders $subject = str_replace( array('{product_name}', '{discount_percent}'), array($product_name, $discount_percent), $subject ); $message = str_replace( array('{product_name}', '{product_url}', '{site_name}', '{old_price}', '{new_price}', '{discount_percent}'), array($product_name, $product_url, get_bloginfo('name'), $formatted_old_price, $formatted_new_price, $discount_percent), $message ); // Set email headers $headers = array(); $from_name = get_bloginfo('name'); $from_email = get_option('woocommerce_email_from_address', get_option('admin_email')); if ($from_email) { $headers[] = 'From: ' . $from_name . ' <' . $from_email . '>'; } $headers[] = 'Content-Type: text/html; charset=UTF-8'; // Send emails foreach ($subscriptions as $subscription) { wp_mail($subscription->email, $subject, nl2br($message), $headers); } $this->log(sprintf( 'Price drop notifications sent: Product ID: %d, Variation ID: %d, Old Price: %s, New Price: %s, Subscribers: %d', $product_id, $variation_id, $formatted_old_price, $formatted_new_price, count($subscriptions) )); } /** * Send stock notifications to subscribers */ private function send_stock_notifications($product_id, $variation_id) { global $wpdb; $table_name = $wpdb->prefix . 'bdfg_stock_subscriptions'; // Get pending subscriptions if ($variation_id > 0) { // For variation products $subscriptions = $wpdb->get_results($wpdb->prepare( "SELECT * FROM $table_name WHERE product_id = %d AND variation_id = %d AND status = 'pending'", $product_id, $variation_id )); } else { // For simple products $subscriptions = $wpdb->get_results($wpdb->prepare( "SELECT * FROM $table_name WHERE product_id = %d AND variation_id = 0 AND status = 'pending'", $product_id )); } if (empty($subscriptions)) { return; } $product = wc_get_product($variation_id > 0 ? $variation_id : $product_id); if (!$product) { return; } $product_name = $product->get_name(); $product_url = get_permalink($product_id); // Add variation URL parameter if needed if ($variation_id > 0) { $product_url = add_query_arg('variation_id', $variation_id, $product_url); } // Add UTM parameters for analytics $product_url = add_query_arg(array( 'utm_source' => 'stock-notifier', 'utm_medium' => 'email', 'utm_campaign' => 'back-in-stock' ), $product_url); // Get email templates $subject = isset($this->settings['notification_email_subject']) ? $this->settings['notification_email_subject'] : __('Product Back in Stock - {product_name}', 'bdfg-stock-notifier'); $message = isset($this->settings['notification_email_template']) ? $this->settings['notification_email_template'] : __("Hello,\n\nGood news! The product you were interested in is back in stock:\n\n{product_name}\n\nYou can purchase it now at: {product_url}\n\nBest regards,\n{site_name}", 'bdfg-stock-notifier'); // Replace placeholders $subject = str_replace( array('{product_name}', '{site_name}'), array($product_name, get_bloginfo('name')), $subject ); $message = str_replace( array('{product_name}', '{product_url}', '{site_name}'), array($product_name, $product_url, get_bloginfo('name')), $message ); // Convert plain text to HTML $html_message = nl2br($message); // Add product image if available $thumbnail_id = $product->get_image_id(); if ($thumbnail_id) { $image_url = wp_get_attachment_image_url($thumbnail_id, 'thumbnail'); if ($image_url) { $image_html = '<div style="margin-bottom: 20px;"><img src="' . esc_url($image_url) . '" alt="' . esc_attr($product_name) . '" style="max-width: 200px; height: auto;"></div>'; $html_message = $image_html . $html_message; } } // Set email headers $headers = array(); $from_name = get_bloginfo('name'); $from_email = get_option('woocommerce_email_from_address', get_option('admin_email')); if ($from_email) { $headers[] = 'From: ' . $from_name . ' <' . $from_email . '>'; } $headers[] = 'Content-Type: text/html; charset=UTF-8'; // Batch processing to prevent server overload $batch_size = 20; $batches = array_chunk($subscriptions, $batch_size); $success_count = 0; $fail_count = 0; foreach ($batches as $batch) { foreach ($batch as $subscription) { $email_sent = wp_mail($subscription->email, $subject, $html_message, $headers); // Update subscription status $wpdb->update( $table_name, array('status' => $email_sent ? 'completed' : 'failed'), array('id' => $subscription->id), array('%s'), array('%d') ); if ($email_sent) { $success_count++; } else { $fail_count++; } } // Small delay between batches if (count($batches) > 1) { usleep(500000); // 0.5 seconds } } // Log the results $this->log(sprintf( 'Stock notification emails sent: Product ID: %d, Variation ID: %d, Success: %d, Failed: %d', $product_id, $variation_id, $success_count, $fail_count )); do_action('bdfg_stock_notifier_emails_sent', $product_id, $variation_id, $success_count, $fail_count); } /** * Log important events */ private function log($message) { if (!isset($this->settings['enable_logs']) || $this->settings['enable_logs'] != 'yes') { return; } $upload_dir = wp_upload_dir(); $log_dir = trailingslashit($upload_dir['basedir']) . 'bdfg-stock-notifier-logs'; if (!file_exists($log_dir)) { wp_mkdir_p($log_dir); } $log_file = $log_dir . '/' . date('Y-m-d') . '.log'; $timestamp = date('Y-m-d H:i:s'); error_log("[{$timestamp}] {$message}\n", 3, $log_file); } /** * Add admin menu items */ public function add_admin_menu() { add_submenu_page( 'woocommerce', __('BDFG Stock Notifier', 'bdfg-stock-notifier'), __('Stock Notifier', 'bdfg-stock-notifier'), 'manage_woocommerce', 'bdfg-stock-notifier', array($this, 'render_admin_page') ); // Add help page add_submenu_page( 'bdfg-stock-notifier', __('BDFG Stock Notifier Help', 'bdfg-stock-notifier'), __('Help & Support', 'bdfg-stock-notifier'), 'manage_woocommerce', 'bdfg-stock-notifier-help', array($this, 'render_help_page') ); } /** * Add plugin action links */ public function add_plugin_action_links($links) { $plugin_links = array( '<a href="' . admin_url('admin.php?page=bdfg-stock-notifier') . '">' . __('Settings', 'bdfg-stock-notifier') . '</a>', ); return array_merge($plugin_links, $links); } /** * Register plugin settings */ public function register_settings() { register_setting('bdfg_stock_notifier_settings', 'bdfg_stock_notifier_settings'); // General settings section add_settings_section( 'bdfg_stock_notifier_general', __('General Settings', 'bdfg-stock-notifier'), array($this, 'render_section_general'), 'bdfg_stock_notifier_settings' ); add_settings_field( 'enable_stock_notifications', __('Enable Stock Notifications', 'bdfg-stock-notifier'), array($this, 'render_field_enable'), 'bdfg_stock_notifier_settings', 'bdfg_stock_notifier_general' ); add_settings_field( 'enable_price_drop', __('Enable Price Drop Notifications', 'bdfg-stock-notifier'), array($this, 'render_field_price_drop'), 'bdfg_stock_notifier_settings', 'bdfg_stock_notifier_general' ); add_settings_field( 'enable_logs', __('Enable Logs', 'bdfg-stock-notifier'), array($this, 'render_field_logs'), 'bdfg_stock_notifier_settings', 'bdfg_stock_notifier_general' ); add_settings_field( 'remove_branding', __('Remove Branding', 'bdfg-stock-notifier'), array($this, 'render_field_branding'), 'bdfg_stock_notifier_settings', 'bdfg_stock_notifier_general' ); // Display settings section add_settings_section( 'bdfg_stock_notifier_display', __('Display Settings', 'bdfg-stock-notifier'), array($this, 'render_section_display'), 'bdfg_stock_notifier_settings' ); add_settings_field( 'display_position', __('Form Position', 'bdfg-stock-notifier'), array($this, 'render_field_position'), 'bdfg_stock_notifier_settings', 'bdfg_stock_notifier_display' ); add_settings_field( 'display_priority', __('Display Priority', 'bdfg-stock-notifier'), array($this, 'render_field_priority'), 'bdfg_stock_notifier_settings', 'bdfg_stock_notifier_display' ); add_settings_field( 'button_color', __('Button Color', 'bdfg-stock-notifier'), array($this, 'render_field_button_color'), 'bdfg_stock_notifier_settings', 'bdfg_stock_notifier_display' ); add_settings_field( 'button_text_color', __('Button Text Color', 'bdfg-stock-notifier'), array($this, 'render_field_button_text_color'), 'bdfg_stock_notifier_settings', 'bdfg_stock_notifier_display' ); // Email settings section add_settings_section( 'bdfg_stock_notifier_email', __('Email Settings', 'bdfg-stock-notifier'), array($this, 'render_section_email'), 'bdfg_stock_notifier_settings' ); add_settings_field( 'notification_email_subject', __('Email Subject', 'bdfg-stock-notifier'), array($this, 'render_field_email_subject'), 'bdfg_stock_notifier_settings', 'bdfg_stock_notifier_email' ); add_settings_field( 'notification_email_template', __('Email Template', 'bdfg-stock-notifier'), array($this, 'render_field_email_template'), 'bdfg_stock_notifier_settings', 'bdfg_stock_notifier_email' ); } /** * Render admin page */ public function render_admin_page() { // Handle tab navigation $active_tab = isset($_GET['tab']) ? sanitize_text_field($_GET['tab']) : 'subscribers'; ?> <div class="wrap"> <h1><?php _e('BDFG Stock Notifier for WooCommerce', 'bdfg-stock-notifier'); ?></h1> <div class="bdfg-admin-header"> <div class="bdfg-admin-logo"> <img src="<?php echo BDFG_STOCK_NOTIFIER_URL . 'assets/img/logo.png'; ?>" alt="BDFG Stock Notifier"> </div> <div class="bdfg-admin-version"> <?php echo sprintf(__('Version %s', 'bdfg-stock-notifier'), BDFG_STOCK_NOTIFIER_VERSION); ?> </div> </div> <h2 class="nav-tab-wrapper"> <a href="<?php echo admin_url('admin.php?page=bdfg-stock-notifier&tab=subscribers'); ?>" class="nav-tab <?php echo $active_tab == 'subscribers' ? 'nav-tab-active' : ''; ?>"> <?php _e('Subscribers', 'bdfg-stock-notifier'); ?> </a> <a href="<?php echo admin_url('admin.php?page=bdfg-stock-notifier&tab=settings'); ?>" class="nav-tab <?php echo $active_tab == 'settings' ? 'nav-tab-active' : ''; ?>"> <?php _e('Settings', 'bdfg-stock-notifier'); ?> </a> <a href="<?php echo admin_url('admin.php?page=bdfg-stock-notifier&tab=stats'); ?>" class="nav-tab <?php echo $active_tab == 'stats' ? 'nav-tab-active' : ''; ?>"> <?php _e('Statistics', 'bdfg-stock-notifier'); ?> </a> <a href="<?php echo admin_url('admin.php?page=bdfg-stock-notifier-help'); ?>" class="nav-tab"> <?php _e('Help', 'bdfg-stock-notifier'); ?> </a> </h2> <?php switch ($active_tab) { case 'settings': $this->render_settings_page(); break; case 'stats': $this->render_stats_page(); break; default: $this->render_subscribers_page(); break; } ?> </div> <?php } /** * Render help page */ public function render_help_page() { ?> <div class="wrap"> <h1><?php _e('BDFG Stock Notifier Help & Support', 'bdfg-stock-notifier'); ?></h1> <div class="bdfg-admin-header"> <div class="bdfg-admin-logo"> <img src="<?php echo BDFG_STOCK_NOTIFIER_URL . 'assets/img/logo.png'; ?>" alt="BDFG Stock Notifier"> </div> <div class="bdfg-admin-version"> <?php echo sprintf(__('Version %s', 'bdfg-stock-notifier'), BDFG_STOCK_NOTIFIER_VERSION); ?> </div> </div> <div class="bdfg-help-wrapper"> <div class="bdfg-help-section"> <h2><?php _e('How to Use', 'bdfg-stock-notifier'); ?></h2> <p><?php _e('The BDFG Stock Notifier plugin automatically adds a subscription form to out-of-stock products on your WooCommerce store. Here\'s how to get the most out of it:', 'bdfg-stock-notifier'); ?></p> <h3><?php _e('Basic Setup', 'bdfg-stock-notifier'); ?></h3> <ol> <li><?php _e('Go to WooCommerce > Stock Notifier > Settings', 'bdfg-stock-notifier'); ?></li> <li><?php _e('Enable stock notifications', 'bdfg-stock-notifier'); ?></li> <li><?php _e('Configure email templates and display settings', 'bdfg-stock-notifier'); ?></li> </ol> <h3><?php _e('Using Shortcode', 'bdfg-stock-notifier'); ?></h3> <p><?php _e('You can add the stock notification form anywhere using the shortcode:', 'bdfg-stock-notifier'); ?></p> <code>[bdfg_stock_notifier product_id="123"]</code> <p><?php _e('Optional parameters:', 'bdfg-stock-notifier'); ?></p> <ul> <li><code>product_id</code> - <?php _e('The ID of the product (required unless used on a product page)', 'bdfg-stock-notifier'); ?></li> <li><code>variation_id</code> - <?php _e('Specific variation ID for variable products', 'bdfg-stock-notifier'); ?></li> <li><code>button_text</code> - <?php _e('Custom text for the submit button', 'bdfg-stock-notifier'); ?></li> <li><code>title</code> - <?php _e('Custom title for the form', 'bdfg-stock-notifier'); ?></li> </ul> </div> <div class="bdfg-help-section"> <h2><?php _e('FAQ', 'bdfg-stock-notifier'); ?></h2> <div class="bdfg-faq-item"> <h3><?php _e('How do email notifications work?', 'bdfg-stock-notifier'); ?></h3> <p><?php _e('When a product comes back in stock (status changes from "out of stock" to "in stock"), all customers who subscribed to notifications for that product will receive an email automatically.', 'bdfg-stock-notifier'); ?></p> </div> <div class="bdfg-faq-item"> <h3><?php _e('Can I customize the notification email?', 'bdfg-stock-notifier'); ?></h3> <p><?php _e('Yes! Go to the Settings tab and modify the email subject and template. You can use these placeholders: {product_name}, {product_url}, and {site_name}.', 'bdfg-stock-notifier'); ?></p> </div> <div class="bdfg-faq-item"> <h3><?php _e('What is the price drop notification feature?', 'bdfg-stock-notifier'); ?></h3> <p><?php _e('This allows subscribers to also receive notifications when a product\'s price is reduced, not just when it comes back in stock. Enable this feature in the Settings tab.', 'bdfg-stock-notifier'); ?></p> </div> <div class="bdfg-faq-item"> <h3><?php _e('Where can I view my subscribers?', 'bdfg-stock-notifier'); ?></h3> <p><?php _e('You can view and manage all subscribers from the Subscribers tab.', 'bdfg-stock-notifier'); ?></p> </div> </div> <div class="bdfg-help-section"> <h2><?php _e('Support', 'bdfg-stock-notifier'); ?></h2> <p><?php _e('If you need assistance with the plugin, please visit our support page:', 'bdfg-stock-notifier'); ?></p> <p><a href="https://beiduofengou.net/support" target="_blank" class="button button-primary"><?php _e('Get Support', 'bdfg-stock-notifier'); ?></a></p> </div> </div> </div> <?php } /** * Render settings page */ private function render_settings_page() { ?> <form method="post" action="options.php"> <?php settings_fields('bdfg_stock_notifier_settings'); do_settings_sections('bdfg_stock_notifier_settings'); submit_button(); ?> </form> <?php } /** * Render subscribers page with filters and search */ private function render_subscribers_page() { global $wpdb; // Process bulk actions if (isset($_POST['bdfg_action']) && isset($_POST['subscription_ids']) && is_array($_POST['subscription_ids'])) { check_admin_referer('bdfg_bulk_action', 'bdfg_nonce'); $action = sanitize_text_field($_POST['bdfg_action']); $ids = array_map('absint', $_POST['subscription_ids']); if (!empty($ids)) { $table_name = $wpdb->prefix . 'bdfg_stock_subscriptions'; $id_list = implode(',', $ids); if ($action === 'delete') { $wpdb->query("DELETE FROM $table_name WHERE id IN ($id_list)"); echo '<div class="notice notice-success"><p>' . __('Selected subscriptions deleted successfully.', 'bdfg-stock-notifier') . '</p></div>'; } elseif ($action === 'mark_completed') { $wpdb->query("UPDATE $table_name SET status = 'completed' WHERE id IN ($id_list)"); echo '<div class="notice notice-success"><p>' . __('Subscriptions marked as completed.', 'bdfg-stock-notifier') . '</p></div>'; } elseif ($action === 'export_csv' && !empty($ids)) { $this->export_subscribers_csv($ids); } } } // Handle search and filters $search = isset($_GET['subscription_search']) ? sanitize_text_field($_GET['subscription_search']) : ''; $status = isset($_GET['status']) ? sanitize_text_field($_GET['status']) : ''; $product_id = isset($_GET['product_id']) ? absint($_GET['product_id']) : 0; // Prepare pagination $per_page = 20; $current_page = isset($_GET['paged']) ? max(1, absint($_GET['paged'])) : 1; $offset = ($current_page - 1) * $per_page; // Build query $table_name = $wpdb->prefix . 'bdfg_stock_subscriptions'; $query = "SELECT * FROM $table_name WHERE 1=1"; $count_query = "SELECT COUNT(*) FROM $table_name WHERE 1=1"; $query_args = array(); if (!empty($search)) { $query .= " AND email LIKE %s"; $count_query .= " AND email LIKE %s"; $query_args[] = '%' . $wpdb->esc_like($search) . '%'; } if (!empty($status)) { $query .= " AND status = %s"; $count_query .= " AND status = %s"; $query_args[] = $status; } if (!empty($product_id)) { $query .= " AND product_id = %d"; $count_query .= " AND product_id = %d"; $query_args[] = $product_id; } // Add order and limit $query .= " ORDER BY date_created DESC LIMIT %d OFFSET %d"; $query_args[] = $per_page; $query_args[] = $offset; // Execute queries $total_items = $wpdb->get_var($wpdb->prepare($count_query, $query_args)); $subscriptions = $wpdb->get_results($wpdb->prepare($query, $query_args)); $total_pages = ceil($total_items / $per_page); // Display subscribers list ?> <div class="bdfg-subscribers-wrapper"> <div class="bdfg-subscribers-filters"> <form method="get"> <input type="hidden" name="page" value="bdfg-stock-notifier"> <input type="hidden" name="tab" value="subscribers"> <div class="bdfg-filters-row"> <div class="bdfg-filter-item"> <input type="text" name="subscription_search" placeholder="<?php _e('Search email...', 'bdfg-stock-notifier'); ?>" value="<?php echo esc_attr($search); ?>"> </div> <div class="bdfg-filter-item"> <select name="status"> <option value=""><?php _e('All Statuses', 'bdfg-stock-notifier'); ?></option> <option value="pending" <?php selected($status, 'pending'); ?>><?php _e('Pending', 'bdfg-stock-notifier'); ?></option> <option value="completed" <?php selected($status, 'completed'); ?>><?php _e('Completed', 'bdfg-stock-notifier'); ?></option> <option value="failed" <?php selected($status, 'failed'); ?>><?php _e('Failed', 'bdfg-stock-notifier'); ?></option> </select> </div> <div class="bdfg-filter-item"> <input type="submit" class="button" value="<?php _e('Filter', 'bdfg-stock-notifier'); ?>"> <?php if (!empty($search) || !empty($status) || !empty($product_id)): ?> <a href="<?php echo admin_url('admin.php?page=bdfg-stock-notifier&tab=subscribers'); ?>" class="button"><?php _e('Reset', 'bdfg-stock-notifier'); ?></a> <?php endif; ?> </div> </div> </form> </div> <div class="bdfg-subscribers-actions"> <div class="tablenav top"> <div class="alignleft actions"> <form method="post"> <?php wp_nonce_field('bdfg_bulk_action', 'bdfg_nonce'); ?> <select name="bdfg_action"> <option value=""><?php _e('Bulk Actions', 'bdfg-stock-notifier'); ?></option> <option value="delete"><?php _e('Delete', 'bdfg-stock-notifier'); ?></option> <option value="mark_completed"><?php _e('Mark as Completed', 'bdfg-stock-notifier'); ?></option> <option value="export_csv"><?php _e('Export as CSV', 'bdfg-stock-notifier'); ?></option> </select> <input type="submit" class="button action" value="<?php _e('Apply', 'bdfg-stock-notifier'); ?>"> </form> </div> <div class="tablenav-pages"> <?php if ($total_pages > 1): ?> <span class="displaying-num"><?php echo sprintf(_n('%s item', '%s items', $total_items, 'bdfg-stock-notifier'), number_format_i18n($total_items)); ?></span> <?php $paginate_args = array( 'base' => add_query_arg('paged', '%#%'), 'format' => '', 'prev_text' => '«', 'next_text' => '»', 'total' => $total_pages, 'current' => $current_page, 'add_args' => array( 'subscription_search' => $search, 'status' => $status, 'product_id' => $product_id, ) ); echo paginate_links($paginate_args); ?> <?php endif; ?> </div> </div> </div> <form method="post"> <?php wp_nonce_field('bdfg_bulk_action', 'bdfg_nonce'); ?> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <th scope="col" class="manage-column column-cb check-column"> <input type="checkbox" id="cb-select-all-1"> </th> <th scope="col" class="manage-column"><?php _e('Email', 'bdfg-stock-notifier'); ?></th> <th scope="col" class="manage-column"><?php _e('Product', 'bdfg-stock-notifier'); ?></th> <th scope="col" class="manage-column"><?php _e('Date', 'bdfg-stock-notifier'); ?></th> <th scope="col" class="manage-column"><?php _e('Status', 'bdfg-stock-notifier'); ?></th> <th scope="col" class="manage-column"><?php _e('Price Drop', 'bdfg-stock-notifier'); ?></th> </tr> </thead> <tbody> <?php if (empty($subscriptions)): ?> <tr> <td colspan="6"><?php _e('No subscriptions found.', 'bdfg-stock-notifier'); ?></td> </tr> <?php else: ?> <?php foreach ($subscriptions as $subscription): ?> <tr> <th scope="row" class="check-column"> <input type="checkbox" name="subscription_ids[]" value="<?php echo esc_attr($subscription->id); ?>"> </th> <td> <?php echo esc_html($subscription->email); ?> <?php if ($subscription->user_id > 0): ?> <span class="bdfg-user-badge"><?php _e('Registered User', 'bdfg-stock-notifier'); ?></span> <?php endif; ?> </td> <td> <?php $product = wc_get_product($subscription->product_id); if ($product) { $product_name = $product->get_name(); if ($subscription->variation_id > 0) { $variation = wc_get_product($subscription->variation_id); if ($variation) { $product_name .= ' - ' . implode(', ', $variation->get_variation_attributes()); } } echo '<a href="' . esc_url(get_edit_post_link($subscription->product_id)) . '">' . esc_html($product_name) . '</a>'; } else { echo __('Product not found', 'bdfg-stock-notifier'); } ?> </td> <td><?php echo date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($subscription->date_created)); ?></td> <td> <?php $status_labels = array( 'pending' => __('Pending', 'bdfg-stock-notifier'), 'completed' => __('Completed', 'bdfg-stock-notifier'), 'failed' => __('Failed', 'bdfg-stock-notifier') ); $status_class = 'bdfg-status-' . $subscription->status; echo '<span class="bdfg-status ' . esc_attr($status_class) . '">' . (isset($status_labels[$subscription->status]) ? $status_labels[$subscription->status] : $subscription->status) . '</span>'; ?> </td> <td> <?php echo $subscription->notify_price_drop ? __('Yes', 'bdfg-stock-notifier') : __('No', 'bdfg-stock-notifier'); ?> </td> </tr> <?php endforeach; ?> <?php endif; ?> </tbody> </table> </form> <div class="tablenav bottom"> <?php if ($total_pages > 1): ?> <div class="tablenav-pages"> <span class="displaying-num"><?php echo sprintf(_n('%s item', '%s items', $total_items, 'bdfg-stock-notifier'), number_format_i18n($total_items)); ?></span> <?php echo paginate_links($paginate_args); ?> </div> <?php endif; ?> </div> </div> <?php } /** * Export subscribers to CSV */ private function export_subscribers_csv($ids) { global $wpdb; $table_name = $wpdb->prefix . 'bdfg_stock_subscriptions'; $id_list = implode(',', array_map('absint', $ids)); $subscriptions = $wpdb->get_results("SELECT * FROM $table_name WHERE id IN ($id_list) ORDER BY date_created DESC"); if (empty($subscriptions)) { return; } // Set headers for CSV download header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename=bdfg-stock-notifier-export-' . date('Y-m-d') . '.csv'); // Create a file pointer $output = fopen('php://output', 'w'); // Set column headers fputcsv($output, array( __('ID', 'bdfg-stock-notifier'), __('Email', 'bdfg-stock-notifier'), __('Product ID', 'bdfg-stock-notifier'), __('Product Name', 'bdfg-stock-notifier'), __('Variation ID', 'bdfg-stock-notifier'), __('Date Created', 'bdfg-stock-notifier'), __('Status', 'bdfg-stock-notifier'), __('User ID', 'bdfg-stock-notifier'), __('Price Drop Notifications', 'bdfg-stock-notifier') )); // Output each subscription as a CSV line foreach ($subscriptions as $subscription) { $product = wc_get_product($subscription->product_id); $product_name = $product ? $product->get_name() : __('Product not found', 'bdfg-stock-notifier'); fputcsv($output, array( $subscription->id, $subscription->email, $subscription->product_id, $product_name, $subscription->variation_id, $subscription->date_created, $subscription->status, $subscription->user_id, $subscription->notify_price_drop ? __('Yes', 'bdfg-stock-notifier') : __('No', 'bdfg-stock-notifier') )); } exit; } /** * Render statistics page */ private function render_stats_page() { global $wpdb; $table_name = $wpdb->prefix . 'bdfg_stock_subscriptions'; // Get basic statistics $total_subscribers = $wpdb->get_var("SELECT COUNT(*) FROM $table_name"); $pending_notifications = $wpdb->get_var("SELECT COUNT(*) FROM $table_name WHERE status = 'pending'"); $completed_notifications = $wpdb->get_var("SELECT COUNT(*) FROM $table_name WHERE status = 'completed'"); $failed_notifications = $wpdb->get_var("SELECT COUNT(*) FROM $table_name WHERE status = 'failed'"); // Get top subscribed products $top_products = $wpdb->get_results( "SELECT product_id, COUNT(*) as subscription_count FROM $table_name GROUP BY product_id ORDER BY subscription_count DESC LIMIT 10" ); // Get recent activity $recent_activity = $wpdb->get_results( "SELECT * FROM $table_name ORDER BY date_created DESC LIMIT 15" ); // Get monthly stats $monthly_stats = $wpdb->get_results( "SELECT DATE_FORMAT(date_created, '%Y-%m') as month, COUNT(*) as count FROM $table_name GROUP BY month ORDER BY month DESC LIMIT 12" ); ?> <div class="bdfg-stats-wrapper"> <div class="bdfg-stats-header"> <h2><?php _e('Stock Notification Statistics', 'bdfg-stock-notifier'); ?></h2> <p><?php _e('Overview of your stock notification subscriptions and activity.', 'bdfg-stock-notifier'); ?></p> </div> <div class="bdfg-stats-summary"> <div class="bdfg-stats-card"> <h3><?php _e('Total Subscribers', 'bdfg-stock-notifier'); ?></h3> <div class="bdfg-stats-number"><?php echo number_format_i18n($total_subscribers); ?></div> </div> <div class="bdfg-stats-card"> <h3><?php _e('Pending Notifications', 'bdfg-stock-notifier'); ?></h3> <div class="bdfg-stats-number"><?php echo number_format_i18n($pending_notifications); ?></div> </div> <div class="bdfg-stats-card"> <h3><?php _e('Sent Notifications', 'bdfg-stock-notifier'); ?></h3> <div class="bdfg-stats-number"><?php echo number_format_i18n($completed_notifications); ?></div> </div> <div class="bdfg-stats-card"> <h3><?php _e('Failed Notifications', 'bdfg-stock-notifier'); ?></h3> <div class="bdfg-stats-number"><?php echo number_format_i18n($failed_notifications); ?></div> </div> </div> <div class="bdfg-stats-content"> <div class="bdfg-stats-column"> <h3><?php _e('Most Wanted Products', 'bdfg-stock-notifier'); ?></h3> <table class="widefat"> <thead> <tr> <th><?php _e('Product', 'bdfg-stock-notifier'); ?></th> <th><?php _e('Subscriptions', 'bdfg-stock-notifier'); ?></th> </tr> </thead> <tbody> <?php if (empty($top_products)): ?> <tr> <td colspan="2"><?php _e('No data available', 'bdfg-stock-notifier'); ?></td> </tr> <?php else: ?> <?php foreach ($top_products as $product_stats): ?> <tr> <td> <?php $product = wc_get_product($product_stats->product_id); if ($product) { echo '<a href="' . esc_url(get_edit_post_link($product_stats->product_id)) . '">' . esc_html($product->get_name()) . '</a>'; } else { echo __('Product not found', 'bdfg-stock-notifier') . ' (ID: ' . $product_stats->product_id . ')'; } ?> </td> <td><?php echo number_format_i18n($product_stats->subscription_count); ?></td> </tr> <?php endforeach; ?> <?php endif; ?> </tbody> </table> </div> <div class="bdfg-stats-column"> <h3><?php _e('Monthly Subscriptions', 'bdfg-stock-notifier'); ?></h3> <table class="widefat"> <thead> <tr> <th><?php _e('Month', 'bdfg-stock-notifier'); ?></th> <th><?php _e('Subscriptions', 'bdfg-stock-notifier'); ?></th> </tr> </thead> <tbody> <?php if (empty($monthly_stats)): ?> <tr> <td colspan="2"><?php _e('No data available', 'bdfg-stock-notifier'); ?></td> </tr> <?php else: ?> <?php foreach ($monthly_stats as $month_stat): ?> <tr> <td> <?php $date = new DateTime($month_stat->month . '-01'); echo $date->format('F Y'); ?> </td> <td><?php echo number_format_i18n($month_stat->count); ?></td> </tr> <?php endforeach; ?> <?php endif; ?> </tbody> </table> </div> </div> </div> <?php } /** * Register REST API endpoints */ public function register_rest_endpoints() { register_rest_route('bdfg-stock-notifier/v1', '/subscriptions', array( 'methods' => 'GET', 'callback' => array($this, 'api_get_subscriptions'), 'permission_callback' => function() { return current_user_can('manage_woocommerce'); } )); register_rest_route('bdfg-stock-notifier/v1', '/stats', array( 'methods' => 'GET', 'callback' => array($this, 'api_get_stats'), 'permission_callback' => function() { return current_user_can('manage_woocommerce'); } )); } /** * API endpoint to get subscriptions */ public function api_get_subscriptions($request) { global $wpdb; $table_name = $wpdb->prefix . 'bdfg_stock_subscriptions'; $per_page = isset($request['per_page']) ? absint($request['per_page']) : 20; $page = isset($request['page']) ? absint($request['page']) : 1; $offset = ($page - 1) * $per_page; $subscriptions = $wpdb->get_results($wpdb->prepare( "SELECT * FROM $table_name ORDER BY date_created DESC LIMIT %d OFFSET %d", $per_page, $offset )); $total_items = $wpdb->get_var("SELECT COUNT(*) FROM $table_name"); foreach ($subscriptions as &$subscription) { $product = wc_get_product($subscription->product_id); $subscription->product_name = $product ? $product->get_name() : __('Product not found', 'bdfg-stock-notifier'); if ($subscription->variation_id > 0) { $variation = wc_get_product($subscription->variation_id); if ($variation) { $subscription->variation_name = implode(', ', $variation->get_variation_attributes()); } } } return array( 'subscriptions' => $subscriptions, 'total' => (int) $total_items, 'pages' => ceil($total_items / $per_page) ); } /** * API endpoint to get stats */ public function api_get_stats() { global $wpdb; $table_name = $wpdb->prefix . 'bdfg_stock_subscriptions'; $stats = array( 'total' => $wpdb->get_var("SELECT COUNT(*) FROM $table_name"), 'pending' => $wpdb->get_var("SELECT COUNT(*) FROM $table_name WHERE status = 'pending'"), 'completed' => $wpdb->get_var("SELECT COUNT(*) FROM $table_name WHERE status = 'completed'"), 'failed' => $wpdb->get_var("SELECT COUNT(*) FROM $table_name WHERE status = 'failed'"), ); return $stats; } /** * Add endpoint for My Account area */ public function add_account_endpoint($query_vars) { $query_vars['stock-notifications'] = 'stock-notifications'; return $query_vars; } /** * Add menu item to My Account menu */ public function add_account_menu_item($items) { // Insert after orders $new_items = array(); foreach ($items as $key => $value) { $new_items[$key] = $value; if ($key === 'orders') { $new_items['stock-notifications'] = __('Stock Notifications', 'bdfg-stock-notifier'); } } return $new_items; } /** * Content for My Account stock notifications page */ public function account_stock_notifications_content() { global $wpdb; $user_id = get_current_user_id(); $user = get_user_by('id', $user_id); if (!$user) { return; } $table_name = $wpdb->prefix . 'bdfg_stock_subscriptions'; // Process unsubscribe requests if (isset($_POST['bdfg_unsubscribe']) && isset($_POST['subscription_id']) && wp_verify_nonce($_POST['bdfg_account_nonce'], 'bdfg_unsubscribe')) { $subscription_id = absint($_POST['subscription_id']); // Verify this subscription belongs to the user $subscription = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d AND (email = %s OR user_id = %d)", $subscription_id, $user->user_email, $user_id )); if ($subscription) { $wpdb->delete( $table_name, array('id' => $subscription_id), array('%d') ); echo '<div class="woocommerce-message">' . __('You have been unsubscribed successfully.', 'bdfg-stock-notifier') . '</div>'; } } // Get user's subscriptions $subscriptions = $wpdb->get_results($wpdb->prepare( "SELECT * FROM $table_name WHERE email = %s OR user_id = %d ORDER BY date_created DESC", $user->user_email, $user_id )); ?> <h2><?php _e('My Stock Notifications', 'bdfg-stock-notifier'); ?></h2> <?php if (empty($subscriptions)): ?> <p><?php _e('You are not currently subscribed to any out-of-stock product notifications.', 'bdfg-stock-notifier'); ?></p> <?php else: ?> <p><?php _e('You will receive an email notification when these products are back in stock.', 'bdfg-stock-notifier'); ?></p> <table class="woocommerce-orders-table woocommerce-MyAccount-orders shop_table shop_table_responsive"> <thead> <tr> <th><?php _e('Product', 'bdfg-stock-notifier'); ?></th> <th><?php _e('Date', 'bdfg-stock-notifier'); ?></th> <th><?php _e('Status', 'bdfg-stock-notifier'); ?></th> <th><?php _e('Actions', 'bdfg-stock-notifier'); ?></th> </tr> </thead> <tbody> <?php foreach ($subscriptions as $subscription): ?> <tr> <td> <?php $product = wc_get_product($subscription->product_id); if ($product) { $product_name = $product->get_name(); if ($subscription->variation_id > 0) { $variation = wc_get_product($subscription->variation_id); if ($variation) { $product_name .= ' - ' . implode(', ', $variation->get_variation_attributes()); } } echo '<a href="' . esc_url(get_permalink($subscription->product_id)) . '">' . esc_html($product_name) . '</a>'; } else { echo __('Product no longer available', 'bdfg-stock-notifier'); } ?> </td> <td> <?php echo date_i18n(get_option('date_format'), strtotime($subscription->date_created)); ?> </td> <td> <?php $status_labels = array( 'pending' => __('Waiting for stock', 'bdfg-stock-notifier'), 'completed' => __('Notification sent', 'bdfg-stock-notifier'), 'failed' => __('Failed', 'bdfg-stock-notifier') ); echo isset($status_labels[$subscription->status]) ? $status_labels[$subscription->status] : $subscription->status; ?> </td> <td> <form method="post"> <?php wp_nonce_field('bdfg_unsubscribe', 'bdfg_account_nonce'); ?> <input type="hidden" name="subscription_id" value="<?php echo esc_attr($subscription->id); ?>"> <button type="submit" name="bdfg_unsubscribe" class="button"><?php _e('Unsubscribe', 'bdfg-stock-notifier'); ?></button> </form> </td> </tr> <?php endforeach; ?> </tbody> </table> <?php endif; ?> <?php } /** * Settings section renderers */ public function render_section_general() { echo '<p>' . __('Configure the general settings for stock notifications.', 'bdfg-stock-notifier') . '</p>'; } public function render_section_display() { echo '<p>' . __('Configure how the notification form appears on product pages.', 'bdfg-stock-notifier') . '</p>'; } public function render_section_email() { echo '<p>' . __('Configure the email template for stock notifications.', 'bdfg-stock-notifier') . '</p>'; echo '<p>' . __('Available placeholders: {product_name}, {product_url}, {site_name}', 'bdfg-stock-notifier') . '</p>'; } /** * Settings field renderers */ public function render_field_enable() { $options = get_option('bdfg_stock_notifier_settings'); $value = isset($options['enable_stock_notifications']) ? $options['enable_stock_notifications'] : 'yes'; ?> <select name="bdfg_stock_notifier_settings[enable_stock_notifications]"> <option value="yes" <?php selected($value, 'yes'); ?>><?php _e('Yes', 'bdfg-stock-notifier'); ?></option> <option value="no" <?php selected($value, 'no'); ?>><?php _e('No', 'bdfg-stock-notifier'); ?></option> </select> <p class="description"><?php _e('Enable or disable stock notification functionality.', 'bdfg-stock-notifier'); ?></p> <?php } public function render_field_price_drop() { $options = get_option('bdfg_stock_notifier_settings'); $value = isset($options['enable_price_drop']) ? $options['enable_price_drop'] : 'no'; ?> <select name="bdfg_stock_notifier_settings[enable_price_drop]"> <option value="yes" <?php selected($value, 'yes'); ?>><?php _e('Yes', 'bdfg-stock-notifier'); ?></option> <option value="no" <?php selected($value, 'no'); ?>><?php _e('No', 'bdfg-stock-notifier'); ?></option> </select> <p class="description"><?php _e('Allow customers to receive notifications when product prices drop.', 'bdfg-stock-notifier'); ?></p> <?php } public function render_field_logs() { $options = get_option('bdfg_stock_notifier_settings'); $value = isset($options['enable_logs']) ? $options['enable_logs'] : 'no'; ?> <select name="bdfg_stock_notifier_settings[enable_logs]"> <option value="yes" <?php selected($value, 'yes'); ?>><?php _e('Yes', 'bdfg-stock-notifier'); ?></option> <option value="no" <?php selected($value, 'no'); ?>><?php _e('No', 'bdfg-stock-notifier'); ?></option> </select> <p class="description"> <?php _e('Enable logging of subscription and notification events.', 'bdfg-stock-notifier'); ?> <?php $upload_dir = wp_upload_dir(); $logs_dir = trailingslashit($upload_dir['basedir']) . 'bdfg-stock-notifier-logs'; echo sprintf(__('Logs are stored in: %s', 'bdfg-stock-notifier'), '<code>' . $logs_dir . '</code>'); ?> </p> <?php } public function render_field_branding() { $options = get_option('bdfg_stock_notifier_settings'); $value = isset($options['remove_branding']) ? $options['remove_branding'] : 'no'; ?> <select name="bdfg_stock_notifier_settings[remove_branding]"> <option value="no" <?php selected($value, 'no'); ?>><?php _e('Show branding', 'bdfg-stock-notifier'); ?></option> <option value="yes" <?php selected($value, 'yes'); ?>><?php _e('Remove branding', 'bdfg-stock-notifier'); ?></option> </select> <p class="description"><?php _e('Show or hide "Powered by BDFG Stock Notifier" text on the notification form.', 'bdfg-stock-notifier'); ?></p> <?php } public function render_field_position() { $options = get_option('bdfg_stock_notifier_settings'); $value = isset($options['display_position']) ? $options['display_position'] : 'woocommerce_single_product_summary'; ?> <select name="bdfg_stock_notifier_settings[display_position]"> <option value="woocommerce_single_product_summary" <?php selected($value, 'woocommerce_single_product_summary'); ?>><?php _e('After product summary', 'bdfg-stock-notifier'); ?></option> <option value="woocommerce_product_meta_end" <?php selected($value, 'woocommerce_product_meta_end'); ?>><?php _e('After product meta', 'bdfg-stock-notifier'); ?></option> <option value="woocommerce_after_add_to_cart_form" <?php selected($value, 'woocommerce_after_add_to_cart_form'); ?>><?php _e('After add to cart form', 'bdfg-stock-notifier'); ?></option> <option value="woocommerce_after_single_product_summary" <?php selected($value, 'woocommerce_after_single_product_summary'); ?>><?php _e('After product', 'bdfg-stock-notifier'); ?></option> <option value="woocommerce_before_add_to_cart_form" <?php selected($value, 'woocommerce_before_add_to_cart_form'); ?>><?php _e('Before add to cart form', 'bdfg-stock-notifier'); ?></option> </select> <p class="description"><?php _e('Choose where to display the notification form on product pages.', 'bdfg-stock-notifier'); ?></p> <?php } public function render_field_priority() { $options = get_option('bdfg_stock_notifier_settings'); $value = isset($options['display_priority']) ? absint($options['display_priority']) : 30; ?> <input type="number" name="bdfg_stock_notifier_settings[display_priority]" value="<?php echo esc_attr($value); ?>" min="1" max="100" step="1"> <p class="description"><?php _e('Display priority within the selected position (lower numbers show first).', 'bdfg-stock-notifier'); ?></p> <?php } public function render_field_button_color() { $options = get_option('bdfg_stock_notifier_settings'); $value = isset($options['button_color']) ? $options['button_color'] : '#0066cc'; ?> <input type="color" name="bdfg_stock_notifier_settings[button_color]" value="<?php echo esc_attr($value); ?>"> <p class="description"><?php _e('Choose the notification button color.', 'bdfg-stock-notifier'); ?></p> <?php } public function render_field_button_text_color() { $options = get_option('bdfg_stock_notifier_settings'); $value = isset($options['button_text_color']) ? $options['button_text_color'] : '#ffffff'; ?> <input type="color" name="bdfg_stock_notifier_settings[button_text_color]" value="<?php echo esc_attr($value); ?>"> <p class="description"><?php _e('Choose the notification button text color.', 'bdfg-stock-notifier'); ?></p> <?php } public function render_field_email_subject() { $options = get_option('bdfg_stock_notifier_settings'); $value = isset($options['notification_email_subject']) ? $options['notification_email_subject'] : __('Product Back in Stock - {product_name}', 'bdfg-stock-notifier'); ?> <input type="text" name="bdfg_stock_notifier_settings[notification_email_subject]" value="<?php echo esc_attr($value); ?>" class="regular-text"> <p class="description"><?php _e('The email subject line. Placeholders: {product_name}, {site_name}', 'bdfg-stock-notifier'); ?></p> <?php } public function render_field_email_template() { $options = get_option('bdfg_stock_notifier_settings'); $value = isset($options['notification_email_template']) ? $options['notification_email_template'] : __("Hello,\n\nGood news! The product you were interested in is back in stock:\n\n{product_name}\n\nYou can purchase it now at: {product_url}\n\nBest regards,\n{site_name}", 'bdfg-stock-notifier'); ?> <textarea name="bdfg_stock_notifier_settings[notification_email_template]" rows="10" class="large-text"><?php echo esc_textarea($value); ?></textarea> <p class="description"><?php _e('The email message template. Placeholders: {product_name}, {product_url}, {site_name}', 'bdfg-stock-notifier'); ?></p> <?php } } // Initialize plugin function bdfg_stock_notifier_init() { // Check if WooCommerce is already installed if (bdfg_stock_notifier_check_woocommerce()) { BDFG_Stock_Notifier::instance(); } } add_action('plugins_loaded', 'bdfg_stock_notifier_init'); // Register activation hook for endpoint creation function bdfg_stock_notifier_add_endpoint() { add_rewrite_endpoint('stock-notifications', EP_ROOT | EP_PAGES); flush_rewrite_rules(); } register_activation_hook(__FILE__, 'bdfg_stock_notifier_add_endpoint'); // Load translation file from languages directory function bdfg_stock_notifier_load_plugin_textdomain() { load_plugin_textdomain('bdfg-stock-notifier', false, dirname(plugin_basename(__FILE__)) . '/languages/'); } add_action('plugins_loaded', 'bdfg_stock_notifier_load_plugin_textdomain');
assets/css/frontend.css
/** * BDFG Stock Notifier for WooCommerce - Frontend Styles * * @package BDFG_Stock_Notifier * @version 2.4.0 */ .bdfg-stock-notification-container { margin: 20px 0; padding: 15px; border: 1px solid #e5e5e5; background-color: #f8f8f8; border-radius: 4px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); transition: all 0.3s ease; } .bdfg-stock-notification-container:hover { border-color: #d5d5d5; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); } .bdfg-stock-notification-container h4 { margin-top: 0; margin-bottom: 10px; font-size: 18px; font-weight: 600; color: #333; } .bdfg-stock-notification-form { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 15px; } .bdfg-stock-notification-email { flex: 1; min-width: 250px; padding: 10px 15px; border: 1px solid #ddd; border-radius: 4px; font-size: 16px; height: 42px; line-height: 1.2; } .bdfg-stock-notification-submit { cursor: pointer; padding: 10px 20px; font-weight: 600; border-radius: 4px; border: none; text-transform: uppercase; letter-spacing: 0.5px; transition: all 0.2s ease; } .bdfg-subscription-response { width: 100%; font-size: 14px; margin: 5px 0; padding: 5px 0; font-weight: 500; } .bdfg-subscription-response.success { color: #3c763d; } .bdfg-subscription-response.error { color: #a94442; } .bdfg-price-drop-option { width: 100%; margin: 5px 0; display: flex; align-items: center; } .bdfg-price-drop-option label { display: flex; align-items: center; font-size: 14px; cursor: pointer; } .bdfg-price-drop-option input[type="checkbox"] { margin-right: 5px; } .bdfg-notifier-branding { width: 100%; text-align: right; font-size: 11px; margin-top: 10px; opacity: 0.7; } .bdfg-notifier-branding a { color: inherit; text-decoration: none; } .bdfg-notifier-branding a:hover { text-decoration: underline; } /* Account Page Styles */ .woocommerce-account .bdfg-status { padding: 3px 8px; border-radius: 3px; font-size: 12px; display: inline-block; } .woocommerce-account .bdfg-status-pending { background: #f8ecd1; color: #d19919; } .woocommerce-account .bdfg-status-completed { background: #e0f5e9; color: #2e9a61; } .woocommerce-account .bdfg-status-failed { background: #fce8e8; color: #d93025; } /* Responsive styles */ @media (max-width: 768px) { .bdfg-stock-notification-form { flex-direction: column; } .bdfg-stock-notification-email, .bdfg-stock-notification-submit { width: 100%; } }
assets/js/frontend.js
相关文章: WooCommerce 产品过滤器
/** * BDFG Stock Notifier for WooCommerce - Frontend Scripts * * @package BDFG_Stock_Notifier * @version 2.4.0 * @author beiduofengou */ jQuery(document).ready(function($) { // Stock notification form submission $('.bdfg-stock-notification-form').on('submit', function(e) { e.preventDefault(); var form = $(this); var emailField = form.find('.bdfg-stock-notification-email'); var responseDiv = form.find('.bdfg-subscription-response'); var submitButton = form.find('.bdfg-stock-notification-submit'); // Validate email var email = emailField.val().trim(); var emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { responseDiv.text(bdfgStockNotifier.i18n.email_invalid) .removeClass('success') .addClass('error') .show(); return; } // Get product info var productId = form.data('product-id'); var productType = form.data('product-type'); var variationId = 0; // For variable products, get the selected variation ID if (productType === 'variable') { variationId = $('input[name="variation_id"]').val(); if (!variationId || variationId == 0) { responseDiv.text(bdfgStockNotifier.i18n.select_options) .removeClass('success') .addClass('error') .show(); return; } } // Get price drop notification preference var notifyPriceDrop = form.find('input[name="bdfg_notify_price_drop"]').is(':checked') ? 1 : 0; // Disable submit button and show loading state submitButton.prop('disabled', true).text('...'); responseDiv.removeClass('success error').empty(); // Send AJAX request $.ajax({ url: bdfgStockNotifier.ajaxUrl, type: 'POST', data: { action: 'bdfg_stock_notifier_subscribe', email: email, product_id: productId, variation_id: variationId, notify_price_drop: notifyPriceDrop, nonce: bdfgStockNotifier.nonce }, success: function(response) { if (response.success) { responseDiv.text(response.data.message) .removeClass('error') .addClass('success') .show(); // Update button text and disable inputs submitButton.text(bdfgStockNotifier.i18n.subscribed).prop('disabled', true); emailField.prop('disabled', true); form.find('input[name="bdfg_notify_price_drop"]').prop('disabled', true); // Track event if Google Analytics exists if (typeof ga === 'function') { ga('send', { hitType: 'event', eventCategory: 'Stock Notifications', eventAction: 'Subscribe', eventLabel: 'Product: ' + productId }); } } else { responseDiv.text(response.data.message) .removeClass('success') .addClass('error') .show(); // If product is already in stock, refresh page if (response.data && response.data.in_stock) { setTimeout(function() { window.location.reload(); }, 1500); } submitButton.prop('disabled', false).text(bdfgStockNotifier.i18n.subscribe); } }, error: function() { responseDiv.text(bdfgStockNotifier.i18n.error) .removeClass('success') .addClass('error') .show(); submitButton.prop('disabled', false).text(bdfgStockNotifier.i18n.subscribe); } }); }); // For variable products, listen for variation selection changes $(document).on('found_variation', function(event, variation) { var container = $('.bdfg-stock-notification-container'); if (variation.is_in_stock) { container.slideUp(200); } else { container.slideDown(200); } }); $(document).on('reset_data', function() { var container = $('.bdfg-stock-notification-container'); // Reset form and display container.find('.bdfg-subscription-response').empty(); container.find('.bdfg-stock-notification-email').prop('disabled', false).val(''); container.find('input[name="bdfg_notify_price_drop"]').prop('disabled', false).prop('checked', true); container.find('.bdfg-stock-notification-submit').prop('disabled', false).text(bdfgStockNotifier.i18n.subscribe); }); // Improve usability with keyboard navigation $('.bdfg-stock-notification-email').on('keypress', function(e) { if (e.which === 13) { // Enter key e.preventDefault(); $(this).closest('form').submit(); } }); });
assets/css/admin.css
/** * BDFG Stock Notifier for WooCommerce - Admin Styles * * @package BDFG_Stock_Notifier * @version 2.4.0 */ .bdfg-admin-header { display: flex; align-items: center; margin: 20px 0; padding-bottom: 20px; border-bottom: 1px solid #eee; } .bdfg-admin-logo { margin-right: 15px; } .bdfg-admin-logo img { max-width: 200px; height: auto; } .bdfg-admin-version { background: #f0f0f0; padding: 3px 8px; border-radius: 4px; font-size: 12px; } /* Subscribers table styles */ .bdfg-subscribers-wrapper { margin-top: 20px; } .bdfg-subscribers-filters { margin-bottom: 20px; padding: 15px; background: #fff; border: 1px solid #e5e5e5; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); } .bdfg-filters-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-end; } .bdfg-filter-item { flex: 1; min-width: 200px; max-width: 300px; } .bdfg-filter-item input[type="text"], .bdfg-filter-item select { width: 100%; } /* Status indicators */ .bdfg-status { display: inline-block; padding: 3px 8px; border-radius: 3px; font-size: 12px; line-height: 1.4; } .bdfg-status-pending { background-color: #f8ecd1; color: #bc8c12; } .bdfg-status-completed { background-color: #e0f5e9; color: #2e9a61; } .bdfg-status-failed { background-color: #fce8e8; color: #d93025; } .bdfg-user-badge { display: inline-block; margin-left: 5px; padding: 1px 5px; font-size: 11px; background: #e2e2e2; border-radius: 3px; vertical-align: middle; } /* Statistics page styles */ .bdfg-stats-wrapper { margin-top: 20px; } .bdfg-stats-header { margin-bottom: 20px; } .bdfg-stats-summary { display: flex; flex-wrap: wrap; gap: 20px; margin-bottom: 30px; } .bdfg-stats-card { flex: 1; min-width: 200px; padding: 20px; background: white; border: 1px solid #e5e5e5; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); text-align: center; } .bdfg-stats-card h3 { margin-top: 0; margin-bottom: 15px; font-size: 16px; color: #666; } .bdfg-stats-number { font-size: 32px; font-weight: 700; color: #333; } .bdfg-stats-content { display: flex; flex-wrap: wrap; gap: 30px; } .bdfg-stats-column { flex: 1; min-width: 300px; } .bdfg-stats-column h3 { margin-top: 0; margin-bottom: 15px; } /* Help page styles */ .bdfg-help-wrapper { margin-top: 20px; } .bdfg-help-section { margin-bottom: 30px; padding: 20px; background: white; border: 1px solid #e5e5e5; border-radius: 4px; } .bdfg-help-section h2 { margin-top: 0; border-bottom: 1px solid #eee; padding-bottom: 10px; } .bdfg-faq-item { margin-bottom: 20px; } .bdfg-faq-item h3 { font-size: 16px; margin-bottom: 5px; } /* Responsive fixes */ @media (max-width: 782px) { .bdfg-stats-card { min-width: 100%; } .bdfg-stats-column { min-width: 100%; } }
相关文章: WooCommerce 客户分析报告插件