这篇文章基于为
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, '"').replace(/'/g, ''');
}
})(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';
四、使用方法
- 将上述代码按文件结构放置到子主题
onenavsub目录中 - 登录 WordPress 后台 → 外观 → 小工具
- 找到 “我的导航” 小工具,拖拽到侧边栏区域
- 可选:勾选 “开启右键快速添加” 以启用右键菜单功能
- 保存并访问前台页面即可看到效果
五、扩展建议
- 数据备份: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 子主题开发中遇到问题,欢迎交流讨论。
© 版权声明
文章版权归作者所有,未经允许请勿转载。
相关文章
暂无评论...