为 OneNav 主题添加”我的导航”小工具

这篇文章基于为 onenavsub 子主题开发”我的导航”自定义小工具的真实经验编写,适用于 OneNav 主题及其子主题。

一、功能概述

“我的导航”是一个可以让用户自由管理个人网址的小工具,支持:

  • 增删改网址:在前台即可完成,无需进入后台
  • 自动同步:登录用户数据自动保存到服务器,游客保存在浏览器本地
  • 右键快速添加:浏览网站时,右键点击任意网址卡片即可一键添加到个人导航
  • 完全独立的站点集:不与主题内置的网址管理混用,专属存储空间

效果预览:

┌─ 我的导航 ─────────────────────┐
│  📁 RSS订阅                    │
│  📁 开发工具                   │
│  📁 ...                        │
│  ┌─────────────────────────┐   │
│  │  [+ 添加网站]            │   │
│  │  [编辑]                  │   │
│  └─────────────────────────┘   │
└────────────────────────────────┘

二、技术方案

数据存储

用户类型 存储方式 存储位置
登录用户 WordPress User Meta usermeta 表,meta_key: io_custom_nav_urls
游客 浏览器 LocalStorage key: io_custom_nav_{widget_id}

架构概览

onenavsub/
├── core/
│   └── myinc.php          ← Widget 定义 + AJAX 处理(核心)
├── assets/
│   ├── js/
│   │   └── sub-app.js      ← 前端逻辑(编辑、增删改、右键菜单)
│   └── css/
│       └── sub-style.css   ← 样式适配
└── functions.php           ← 注册 JS/CSS 加载

三、实现步骤

第 1 步:注册小工具

core/myinc.php 中定义 Widget 类:

<?php
// core/myinc.php

class IO_Custom_Nav_Widget extends WP_Widget {

    public function __construct() {
        parent::__construct(
            'io_custom_nav',
            '我的导航',
            array('description' => '用户自定义网址导航小工具')
        );
    }

    public function widget($args, $instance) {
        echo $args['before_widget'];
        // 渲染小工具内容
        $this->render_widget($args, $instance);
        echo $args['after_widget'];
    }

    public function form($instance) {
        // 后台设置表单
        $quick_add = !empty($instance['quick_add']) ? 1 : 0;
        ?>
        <p>
            <label>
                <input type="checkbox" <?php checked($quick_add, 1); ?>
                    name="<?php echo $this->get_field_name('quick_add'); ?>"
                    value="1">
                开启右键快速添加
            </label>
        </p>
        <?php
    }

    private function render_widget($args, $instance) {
        $widget_id = $args['widget_id'] ?? '';
        $quick_add = !empty($instance['quick_add']) ? '1' : '0';
        $is_user_logged_in = is_user_logged_in();
        ?>
        <div class="io-custom-nav user-custome-style"
             data-widget-id="<?php echo esc_attr($widget_id); ?>"
             data-quick-add="<?php echo esc_attr($quick_add); ?>"
             data-logged-in="<?php echo $is_user_logged_in ? '1' : '0'; ?>">
            <div class="io-custom-nav-inner">
                <div class="io-custom-nav-header">
                    <span class="io-custom-nav-title">我的导航</span>
                    <div class="io-custom-nav-actions">
                        <button type="button" class="btn io-custom-nav-add-btn">+ 添加</button>
                        <button type="button" class="btn io-custom-nav-toggle-edit">编辑</button>
                    </div>
                </div>
                <div class="io-custom-nav-sites"></div>
            </div>
        </div>
        <?php
    }
}

// 注册小工具
add_action('widgets_init', function() {
    register_widget('IO_Custom_Nav_Widget');
});

第 2 步:添加 AJAX 处理器

myinc.php 中继续添加:

// 保存
add_action('wp_ajax_io_custom_nav_save', 'io_handle_custom_nav_save');
add_action('wp_ajax_nopriv_io_custom_nav_save', 'io_handle_custom_nav_save');

function io_handle_custom_nav_save() {
    check_ajax_referer('io_custom_nav_nonce', 'nonce');

    $raw = isset($_POST['urls']) ? $_POST['urls'] : '';
    $urls = json_decode(stripslashes($raw), true);
    if (!is_array($urls)) $urls = array();

    // 数据清理和限制
    $urls = array_slice($urls, 0, 100); // 最多 100 条
    $clean = array();
    foreach ($urls as $item) {
        $clean[] = array(
            'name' => mb_substr(sanitize_text_field($item['name'] ?? ''), 0, 100),
            'url'  => esc_url_raw(mb_substr($item['url'] ?? '', 0, 500)),
        );
    }

    if (is_user_logged_in()) {
        update_user_meta(get_current_user_id(), 'io_custom_nav_urls', $clean);
    }

    wp_send_json_success(array('urls' => $clean));
}

// 加载
add_action('wp_ajax_io_custom_nav_load', 'io_handle_custom_nav_load');
add_action('wp_ajax_nopriv_io_custom_nav_load', 'io_handle_custom_nav_load');

function io_handle_custom_nav_load() {
    check_ajax_referer('io_custom_nav_nonce', 'nonce');

    if (is_user_logged_in()) {
        $urls = get_user_meta(get_current_user_id(), 'io_custom_nav_urls', true);
        wp_send_json_success(array('urls' => $urls ?: array()));
    } else {
        wp_send_json_success(array('urls' => array()));
    }
}

// 传递 AJAX 参数到前端
add_action('wp_enqueue_scripts', function() {
    wp_localize_script('sub-app', 'ioCustomNav', array(
        'ajaxUrl' => admin_url('admin-ajax.php'),
        'nonce'   => wp_create_nonce('io_custom_nav_nonce'),
        'restUrl' => rest_url(),
    ));
});

第 3 步:前端 JavaScript

assets/js/sub-app.js 中实现前端逻辑:

// sub-app.js — "我的导航"小工具前端逻辑

(function($) {
    'use strict';

    function initCustomNavWidget(widgetEl) {
        var widgetId = widgetEl.dataset.widgetId;
        var quickAdd = widgetEl.dataset.quickAdd === '1';
        var loggedIn = widgetEl.dataset.loggedIn === '1';
        var sitesContainer = widgetEl.querySelector('.io-custom-nav-sites');
        var editBtn = widgetEl.querySelector('.io-custom-nav-toggle-edit');
        var addBtn = widgetEl.querySelector('.io-custom-nav-add-btn');
        var isEditing = false;

        // 加载数据
        function loadSites() {
            if (loggedIn) {
                // AJAX 从服务器加载
                $.post(ioCustomNav.ajaxUrl, {
                    action: 'io_custom_nav_load',
                    nonce: ioCustomNav.nonce
                }, function(res) {
                    if (res.success) renderSites(res.data.urls);
                });
            } else {
                // 游客从 localStorage 加载
                var stored = localStorage.getItem('io_custom_nav_' + widgetId);
                renderSites(stored ? JSON.parse(stored) : []);
            }
        }

        // 渲染站点列表
        function renderSites(urls) {
            if (!urls || urls.length === 0) {
                sitesContainer.innerHTML = '<div class="io-custom-nav-empty">暂无收藏的网址,点击"添加"开始</div>';
                return;
            }
            var html = '';
            urls.forEach(function(item, index) {
                html += '<div class="io-custom-site" data-index="' + index + '">';
                if (isEditing) {
                    html += '<button class="io-custom-site-del" data-index="' + index + '">×</button>';
                    html += '<div class="io-custom-site-edit-content">' +
                        '<span class="io-custom-site-name">' + escHtml(item.name || item.url) + '</span>' +
                        '</div>';
                } else {
                    html += '<a href="' + escAttr(item.url) + '" target="_blank" rel="nofollow">' +
                        '<span class="io-custom-site-name">' + escHtml(item.name || item.url) + '</span>' +
                        '</a>';
                }
                html += '</div>';
            });
            sitesContainer.innerHTML = html;
            bindSiteEvents();
        }

        // 编辑模式切换
        editBtn.addEventListener('click', function(e) {
            e.preventDefault();
            isEditing = !isEditing;
            editBtn.textContent = isEditing ? '完成' : '编辑';
            // 重新渲染(切换编辑状态显示删除按钮)
            // ... 调用 renderSites
        });

        // 右键快速添加(全局)
        if (quickAdd) {
            document.addEventListener('contextmenu', function(e) {
                var target = e.target.closest('.posts-item.sites-item');
                if (!target) return;
                // 排除小工具内部的卡片
                if (target.closest('.io-custom-nav')) return;

                e.preventDefault();
                var nameEl = target.querySelector('.site-name');
                var linkEl = target.querySelector('a[href]');
                var name = nameEl ? nameEl.textContent.trim() : '';
                var url = linkEl ? linkEl.getAttribute('href') : '';

                // 显示自定义上下文菜单
                showContextMenu(e.clientX, e.clientY, name, url);
            });
        }

        loadSites();
    }

    // DOM 就绪后初始化所有小工具实例
    $(document).on('ready', function() {
        document.querySelectorAll('.io-custom-nav.user-custome-style').forEach(initCustomNavWidget);
    });

    // 辅助函数
    function escHtml(str) {
        var div = document.createElement('div');
        div.textContent = str;
        return div.innerHTML;
    }
    function escAttr(str) {
        return str.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
    }

})(jQuery);

第 4 步:CSS 样式

assets/css/sub-style.css 中添加样式:

/* 我的导航容器 — 侧边栏小工具适配 */
.io-custom-nav.user-custome-style {
    margin-top: 1.25rem;
    margin-bottom: 1.5rem;
}

.io-custom-nav-inner {
    background: var(--main-bg-color, #fff);
    border-radius: 12px;
    box-shadow: var(--main-shadow, 0 1px 3px rgba(0,0,0,0.08));
    overflow: hidden;
    transition: box-shadow 0.2s;
}

/* 当外层已经是 .card 时(侧边栏 widget),移除内层背景/阴影 */
.card .io-custom-nav-inner {
    background: none;
    box-shadow: none;
}

/* 标题栏 */
.io-custom-nav-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0.75rem 1rem;
    position: relative;
}
.io-custom-nav-header::before {
    content: '';
    position: absolute;
    bottom: 0;
    left: 1rem;
    right: 1rem;
    height: 1px;
    background: var(--border-color, rgba(0,0,0,0.06));
}
.io-custom-nav-title {
    font-weight: 600;
    font-size: 0.95rem;
    color: var(--theme-color, #425AEF);
}

/* 站点卡片 — 对齐父主题 posts-item 视觉 */
.io-custom-site {
    display: block;
    padding: 0.5rem 1rem;
    border-radius: 8px;
    transition: all 0.2s ease;
    margin: 0.25rem 0.5rem;
    cursor: pointer;
    box-shadow: inset 0 1px 2px rgba(0,0,0,0.02);
}
.io-custom-site:hover {
    box-shadow: var(--muted-shadow, 0 4px 12px rgba(0,0,0,0.08));
    transform: translateY(-1px);
    background: var(--item-hover-bg, rgba(0,0,0,0.02));
}

/* 暗色模式 */
.io-black-mode .io-custom-nav-inner {
    background: var(--main-bg-color, #1a1a2e);
}
.io-black-mode .io-custom-site:hover {
    background: rgba(255,255,255,0.04);
}

第 5 步:在 functions.php 中注册

// functions.php

// 加载子主题 JS
add_action('wp_enqueue_scripts', function() {
    wp_enqueue_script(
        'sub-app',
        get_stylesheet_directory_uri() . '/assets/js/sub-app.js',
        array('jquery'),
        filemtime(get_stylesheet_directory() . '/assets/js/sub-app.js'),
        true
    );
});

// 加载子主题 CSS
add_action('wp_enqueue_scripts', function() {
    wp_enqueue_style(
        'sub-style',
        get_stylesheet_directory_uri() . '/assets/css/sub-style.css',
        array(),
        filemtime(get_stylesheet_directory() . '/assets/css/sub-style.css')
    );
});

// 引入核心文件
require_once get_stylesheet_directory() . '/core/myinc.php';

四、使用方法

  1. 将上述代码按文件结构放置到子主题 onenavsub 目录中
  2. 登录 WordPress 后台 → 外观 → 小工具
  3. 找到 “我的导航” 小工具,拖拽到侧边栏区域
  4. 可选:勾选 “开启右键快速添加” 以启用右键菜单功能
  5. 保存并访问前台页面即可看到效果

五、扩展建议

  • 数据备份:User Meta 存储的数据会随 WordPress 数据库备份,无需额外操作
  • 批量编辑:修改 array_slice($urls, 0, 100) 中 100 的值可调整上限
  • 多小工具实例:同一个页面可放置多个”我的导航”小工具,数据独立存储(按 widget ID 区分)
  • 迁移到 REST API:如需进一步解耦,可将 AJAX 端点改为 WP REST API 路由

六、完整文件结构

最终子主题结构如下:

onenavsub/
├── assets/
│   ├── css/
│   │   └── sub-style.css      ← 包含小工具样式
│   └── js/
│       └── sub-app.js         ← 包含小工具前端逻辑
├── core/
│   └── myinc.php              ← Widget 注册 + AJAX 处理器
├── functions.php              ← 注册资源加载
├── style.css
└── screenshot.jpg

本文基于真实开发记录编写,代码已通过端到端测试。如果你在 OneNav 子主题开发中遇到问题,欢迎交流讨论。

© 版权声明

相关文章

暂无评论

none
暂无评论...