<?php
require_once get_template_directory() . '/inc/register-post-type.php';
if (is_admin()) {
require_once get_template_directory() . '/inc/admin-category-icons.php';
}
register_nav_menus(['primary' => '主导航菜单']);
function theme_enqueue_scripts() {
wp_dequeue_style('wp-block-library');
wp_dequeue_style('wp-block-library-theme');
wp_dequeue_style('global-styles');
wp_enqueue_style('tailwindcss', 'https://cdn.tailwindcss.com', array(), '3', 'all');
wp_enqueue_style('bootstrap-icons', get_template_directory_uri() . '/assets/vendor/bootstrap-icons/bootstrap-icons.min.css', array(), '1.11.3', 'all');
wp_enqueue_style('theme-style', get_stylesheet_uri(), array('tailwindcss', 'bootstrap-icons'), '1.0', 'all');
if (is_front_page() || is_home()) {
wp_enqueue_script('theme-app', get_template_directory_uri() . '/assets/js/app.js', array(), '1.1', true);
wp_localize_script('theme-app', 'WP_API_Settings', array(
'root' => esc_url_raw(rest_url()),
'nonce' => wp_create_nonce('wp_rest'),
'themeDir' => get_template_directory_uri(),
'iconService' => get_option('icon_service_url', 'https://f1.allesedv.com/16/%s/favicon.ico'),
'iconFallback' => get_option('icon_fallback_url', ''),
'defaultTheme' => get_option('default_theme', 'auto'),
'logos' => array(
'lightRect' => esc_url(get_option('logo_light_rect', '')),
'darkRect' => esc_url(get_option('logo_dark_rect', '')),
'lightSquare' => esc_url(get_option('logo_light_square', '')),
'darkSquare' => esc_url(get_option('logo_dark_square', '')),
),
));
}
}
add_action('wp_enqueue_scripts', 'theme_enqueue_scripts', 100);
* 后台顶级菜单:主题设置
* 子菜单:常规设置(默认页)、图标设置
*/
add_action('admin_menu', function() {
add_menu_page(
'主题设置',
'主题设置',
'manage_options',
'navi-settings', // 顶级 slug,首个子页复用此 slug,避免出现重复菜单项
'navi_render_general_page',
'dashicons-star-filled',
58
);
add_submenu_page(
'navi-settings',
'常规设置',
'常规设置',
'manage_options',
'navi-settings', // 与顶级 slug 相同
'navi_render_general_page'
);
add_submenu_page(
'navi-settings',
'站点 LOGO',
'站点 LOGO',
'manage_options',
'navi-logo',
'navi_render_logo_page'
);
add_submenu_page(
'navi-settings',
'侧边栏设置',
'侧边栏设置',
'manage_options',
'navi-sidebar',
'navi_render_sidebar_page'
);
});
* 「网址」菜单下追加「网址检查」子项(与 添加新网址、网址分类 同级)
*/
add_action('admin_menu', function() {
add_submenu_page(
'edit.php?post_type=nav_link', // 父级为「网址」CPT 菜单
'网址检查',
'网址检查',
'edit_posts',
'navi-check',
'navi_render_check_page'
);
}, 20);
add_action('admin_init', function() {
register_setting('navi_general_group', 'default_theme', array(
'type' => 'string',
'default' => 'auto',
'sanitize_callback' => function($v) {
return in_array($v, array('auto', 'light', 'dark'), true) ? $v : 'auto';
},
));
register_setting('navi_general_group', 'icon_service_url', array(
'type' => 'string',
'default' => 'https://f1.allesedv.com/16/%s/favicon.ico',
'sanitize_callback' => 'esc_url_raw',
));
register_setting('navi_general_group', 'icon_fallback_url', array(
'type' => 'string',
'default' => '',
'sanitize_callback' => 'esc_url_raw',
));
});
* 主题设置页共享样式(卡片布局 + 胶囊开关),仅在本主题设置页输出
*/
function navi_admin_style() {
?>
<style>
.navi-admin{max-width:980px}
.navi-card{background:
.navi-card > .title{margin:0 0 2px;padding:0;font-size:15px;line-height:1.4}
.navi-card .navi-sub{color:
.navi-admin .form-table{margin-top:6px}
.navi-admin .form-table th{padding-left:2px}
.navi-admin .submit{padding-left:2px}
.navi-pills{display:flex;flex-wrap:wrap;gap:12px}
.navi-pill{position:relative;display:inline-flex;align-items:center;gap:9px;padding:9px 16px 9px 14px;border-radius:999px;border:1.5px solid
.navi-pill:hover{border-color:
.navi-pill .dashicons{font-size:16px;width:16px;height:16px;line-height:1.2}
.navi-pill .navi-pill-state{font-size:11px;line-height:1;padding:3px 9px;border-radius:999px;background:rgba(0,0,0,.07);color:
.navi-pill input[type=checkbox]{position:absolute;opacity:0;width:0;height:0;margin:0}
.navi-pill.is-on{background:linear-gradient(135deg,#2271b1,#135e96);border-color:transparent;color:
.navi-pill.is-on:hover{color:
.navi-pill.is-on .navi-pill-state{background:rgba(255,255,255,.25);color:
.navi-pill-actions{margin:14px 0 0}
.navi-pill-actions .button{margin-right:8px}
.nav-logo-preview{position:relative}
.nav-logo-preview .nav-logo-clear{position:absolute;top:4px;right:4px;width:22px;height:22px;border-radius:50%;background:rgba(0,0,0,.55);color:
.nav-logo-preview .nav-logo-clear:hover{background:
.nav-logo-preview.has-image .nav-logo-clear{display:flex}
</style>
<?php
}
function navi_render_general_page() {
navi_admin_style();
?>
<div class="wrap navi-admin">
<h1>常规设置</h1>
<form method="post" action="options.php">
<?php
settings_fields('navi_general_group');
?>
<div class="navi-card">
<table class="form-table" role="presentation">
<tr>
<th scope="row"><label for="default_theme">默认主题模式</label></th>
<td>
<select name="default_theme" id="default_theme">
<option value="auto" <?php selected(get_option('default_theme', 'auto'), 'auto'); ?>>跟随系统</option>
<option value="light" <?php selected(get_option('default_theme', 'auto'), 'light'); ?>>浅色</option>
<option value="dark" <?php selected(get_option('default_theme', 'auto'), 'dark'); ?>>深色</option>
</select>
<p class="description">新访客首次打开时使用的明暗主题,用户手动切换后以浏览器本地记录为准。</p>
</td>
</tr>
<tr>
<th scope="row"><label for="icon_service_url">Favicon 备用服务</label></th>
<td>
<input type="url" name="icon_service_url" id="icon_service_url"
value="<?php echo esc_attr(get_option('icon_service_url', 'https://f1.allesedv.com/16/%s/favicon.ico')); ?>"
class="regular-text">
<p class="description">
导航卡片图标自动获取顺序:① 网站自身 favicon → ② <strong>本备用服务</strong> → ③ 兜底占位图。<br>
用 <code>%s</code> 代替域名,国内推荐 <code>https://f1.allesedv.com/16/%s/favicon.ico</code>。
</p>
</td>
</tr>
<tr>
<th scope="row"><label for="icon_fallback_url">兜底图标 URL</label></th>
<td>
<input type="url" name="icon_fallback_url" id="icon_fallback_url"
value="<?php echo esc_attr(get_option('icon_fallback_url')); ?>"
class="regular-text" placeholder="留空使用内置地球图标">
<p class="description">所有网站图标均加载失败时显示的统一占位图,建议 SVG/PNG 正方形。</p>
</td>
</tr>
</table>
</div>
<?php submit_button('保存常规设置'); ?>
</form>
</div>
<?php
}
/* ---------- 站点 LOGO ---------- */
add_action('admin_init', function() {
register_setting('navi_logo_group', 'logo_light_rect', array(
'type' => 'string',
'default' => '',
'sanitize_callback' => 'esc_url_raw',
));
register_setting('navi_logo_group', 'logo_dark_rect', array(
'type' => 'string',
'default' => '',
'sanitize_callback' => 'esc_url_raw',
));
register_setting('navi_logo_group', 'logo_light_square', array(
'type' => 'string',
'default' => '',
'sanitize_callback' => 'esc_url_raw',
));
register_setting('navi_logo_group', 'logo_dark_square', array(
'type' => 'string',
'default' => '',
'sanitize_callback' => 'esc_url_raw',
));
register_setting('navi_logo_group', 'site_favicon', array(
'type' => 'string',
'default' => '',
'sanitize_callback' => 'esc_url_raw',
));
});
* 渲染一个 LOGO 上传字段(缩略图预览 + wp.media 上传 + URL 输入 + 删除)
* $builtin:该字段的内置兜底图 URL,默认显示在预览中(与 favicon 行为一致)
*/
function nav_logo_field($name, $label, $hint, $builtin = '') {
$value = esc_url(get_option($name));
$preview = $value ? $value : $builtin;
?>
<tr>
<th scope="row"><label for="<?php echo esc_attr($name); ?>"><?php echo esc_html($label); ?></label></th>
<td>
<div class="nav-logo-field" data-target="<?php echo esc_attr($name); ?>" data-builtin="<?php echo esc_attr($builtin); ?>">
<div class="nav-logo-preview <?php echo $preview ? 'has-image' : ''; ?>" style="width:220px;height:80px;border:1px dashed #c3c4c7;border-radius:4px;display:flex;align-items:center;justify-content:center;margin-bottom:8px;background:#fafafa;overflow:hidden;">
<?php if ($preview) : ?>
<img src="<?php echo esc_url($preview); ?>" style="max-width:100%;max-height:100%;object-fit:contain;">
<?php else : ?>
<span style="color:#a7aaad;font-size:12px;">未设置(将使用主题内置 Logo)</span>
<?php endif; ?>
<button type="button" class="nav-logo-clear" title="删除图片">×</button>
</div>
<input type="hidden" name="<?php echo esc_attr($name); ?>" class="nav-logo-input"
id="<?php echo esc_attr($name); ?>" value="<?php echo $value; ?>">
<button type="button" class="button nav-logo-upload">上传</button>
<button type="button" class="button nav-logo-delete">删除</button>
<p class="description"><?php echo esc_html($hint); ?></p>
</div>
</td>
</tr>
<?php
}
/**
* 渲染 favicon 字段(32×32 预览 + 上传 + 删除),默认回退到主题内置 assets/logo.ico
*/
function nav_favicon_field() {
$value = esc_url(get_option('site_favicon'));
$builtin = get_template_directory_uri() . '/assets/logo.ico';
$preview = $value ? $value : $builtin;
?>
<tr>
<th scope="row"><label for="site_favicon">站点 Favicon</label></th>
<td>
<div class="nav-logo-field" data-target="site_favicon" data-builtin="<?php echo esc_attr($builtin); ?>">
<div class="nav-logo-preview <?php echo $preview ? 'has-image' : ''; ?>" style="width:48px;height:48px;border:1px dashed #c3c4c7;border-radius:4px;display:flex;align-items:center;justify-content:center;margin-bottom:8px;background:#fafafa;overflow:hidden;">
<img src="<?php echo esc_url($preview); ?>" style="max-width:100%;max-height:100%;object-fit:contain;">
<button type="button" class="nav-logo-clear" title="删除图片">×</button>
</div>
<input type="hidden" name="site_favicon" class="nav-logo-input" id="site_favicon" value="<?php echo $value; ?>">
<button type="button" class="button nav-logo-upload">上传</button>
<button type="button" class="button nav-logo-delete">删除</button>
<p class="description">浏览器标签页图标,建议 32×32 或 64×64 的 .ico/.png 文件;留空使用主题内置 <code>assets/logo.ico</code>。</p>
</div>
</td>
</tr>
<?php
}
function navi_render_logo_page() {
wp_enqueue_media();
navi_admin_style();
?>
<div class="wrap navi-admin">
<h1>站点 LOGO</h1>
<form method="post" action="options.php">
<?php
settings_fields('navi_logo_group');
?>
<div class="navi-card">
<h2 class="title">Favicon</h2>
<p class="navi-sub">浏览器标签页图标,默认使用主题内置 logo.ico。</p>
<table class="form-table" role="presentation">
<?php nav_favicon_field(); ?>
</table>
</div>
<div class="navi-card">
<h2 class="title">四套 LOGO 组合</h2>
<p class="navi-sub">亮色/暗色主题与横版/方形 LOGO 组合使用,为空时自动回退到主题内置 Logo。</p>
<table class="form-table" role="presentation">
<?php
$navi_assets = get_template_directory_uri() . '/assets';
nav_logo_field('logo_light_rect', '亮色主题 Logo(横版)', '展开侧边栏时使用,建议高≤80px、长≤360px 的横版图,白色/浅色背景可见。', $navi_assets . '/logohen.png');
nav_logo_field('logo_dark_rect', '暗色主题 Logo(横版)', '暗色主题展开侧边栏时使用;留空时依次回退:亮色横版 → 内置 logohen@2.png。', $navi_assets . '/logohen@2.png');
nav_logo_field('logo_light_square', '亮色主题方形 Logo', '收起侧边栏时使用,建议 80×80 方形图标。', $navi_assets . '/logo.png');
nav_logo_field('logo_dark_square', '暗色主题方形 Logo', '暗色主题收起侧边栏时使用;留空则复用亮色方形。', $navi_assets . '/logo.png');
?>
</table>
</div>
<?php submit_button('保存 LOGO 设置'); ?>
</form>
</div>
<script>
jQuery(function($) {
var PLACEHOLDER = '<span style="color:#a7aaad;font-size:12px;">未设置(将使用主题内置 Logo)</span>';
var CLEAR_BTN = '<button type="button" class="nav-logo-clear" title="删除图片">×</button>';
function renderPreview($field, url) {
var $preview = $field.find('.nav-logo-preview');
var builtin = $field.data('builtin');
var shown = url || builtin;
if (shown) {
$preview.html('<img src="' + (url || builtin) + '" style="max-width:100%;max-height:100%;object-fit:contain;">' + CLEAR_BTN);
$preview.addClass('has-image');
} else {
$preview.html(PLACEHOLDER);
$preview.removeClass('has-image');
}
}
$('.nav-logo-field').each(function() {
var $field = $(this);
var $input = $field.find('.nav-logo-input');
$field.find('.nav-logo-upload').on('click', function(e) {
e.preventDefault();
var frame = wp.media({
title: '选择或上传图片',
button: { text: '使用此图片' },
multiple: false
});
frame.on('select', function() {
var attachment = frame.state().get('selection').first().toJSON();
$input.val(attachment.url);
renderPreview($field, attachment.url);
});
frame.open();
});
$field.find('.nav-logo-delete').on('click', function(e) {
e.preventDefault();
$input.val('');
renderPreview($field, '');
});
$field.on('click', '.nav-logo-clear', function(e) {
e.preventDefault();
e.stopPropagation();
$input.val('');
renderPreview($field, '');
});
});
});
</script>
<?php
}
add_action('admin_init', function() {
foreach (array('sidebar_show_latest', 'sidebar_show_hotcats', 'sidebar_show_ranking', 'sidebar_show_siteinfo') as $opt) {
register_setting('navi_sidebar_group', $opt, array(
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
));
}
register_setting('navi_sidebar_group', 'sidebar_site_title', array(
'type' => 'string',
'default' => '小辰导航',
'sanitize_callback' => 'sanitize_text_field',
));
register_setting('navi_sidebar_group', 'site_intro', array(
'type' => 'string',
'default' => '',
'sanitize_callback' => 'sanitize_textarea_field',
));
register_setting('navi_sidebar_group', 'site_qq', array(
'type' => 'string',
'default' => '',
'sanitize_callback' => 'sanitize_text_field',
));
register_setting('navi_sidebar_group', 'site_copyright', array(
'type' => 'string',
'default' => '',
'sanitize_callback' => 'sanitize_text_field',
));
register_setting('navi_sidebar_group', 'site_icp', array(
'type' => 'string',
'default' => '',
'sanitize_callback' => 'sanitize_text_field',
));
});
function navi_render_sidebar_page() {
navi_admin_style();
$modules = array(
'sidebar_show_latest' => array('最新收录', 'dashicons-clock'),
'sidebar_show_hotcats' => array('热门分类', 'dashicons-star-filled'),
'sidebar_show_ranking' => array('点击排行', 'dashicons-chart-bar'),
'sidebar_show_siteinfo'=> array('站点信息', 'dashicons-info-outline'),
);
?>
<div class="wrap navi-admin">
<h1>侧边栏设置</h1>
<form method="post" action="options.php">
<?php settings_fields('navi_sidebar_group'); ?>
<div class="navi-card">
<h2 class="title">显示模块</h2>
<p class="navi-sub">点击胶囊开关首页右侧边栏对应模块;关闭后前台完全不渲染,不会留下空白占位。</p>
<div class="navi-pills" id="naviModulePills">
<?php foreach ($modules as $key => $m) :
$on = ((int) get_option($key, 1)) === 1;
?>
<input type="hidden" name="<?php echo esc_attr($key); ?>" value="0">
<label class="navi-pill<?php echo $on ? ' is-on' : ''; ?>">
<span class="dashicons <?php echo esc_attr($m[1]); ?>" aria-hidden="true"></span>
<span><?php echo esc_html($m[0]); ?></span>
<span class="navi-pill-state"><?php echo $on ? '显示' : '隐藏'; ?></span>
<input type="checkbox" name="<?php echo esc_attr($key); ?>" value="1"
<?php checked($on); ?>>
</label>
<?php endforeach; ?>
</div>
<p class="navi-pill-actions">
<button type="button" class="button button-small" data-navi-all="1">全部显示</button>
<button type="button" class="button button-small" data-navi-all="0">全部隐藏</button>
</p>
</div>
<div class="navi-card">
<h2 class="title">站点信息卡片文案</h2>
<p class="navi-sub">显示在右侧边栏「站点信息」卡片中,需开启上方「站点信息」胶囊。</p>
<table class="form-table" role="presentation">
<tr>
<th scope="row"><label for="sidebar_site_title">卡片标题</label></th>
<td>
<input type="text" name="sidebar_site_title" id="sidebar_site_title"
value="<?php echo esc_attr(get_option('sidebar_site_title', '小辰导航')); ?>"
class="regular-text">
<p class="description">卡片顶部标题,默认「小辰导航」。</p>
</td>
</tr>
<tr>
<th scope="row"><label for="site_intro">站点简介</label></th>
<td>
<textarea name="site_intro" id="site_intro" rows="4"
class="large-text"><?php echo esc_textarea(navi_get_intro()); ?></textarea>
<p class="description">未自定义时显示默认简介,可直接在此修改;清空后保存将恢复默认简介。</p>
</td>
</tr>
<tr>
<th scope="row"><label for="site_qq">联系 QQ</label></th>
<td>
<input type="text" name="site_qq" id="site_qq"
value="<?php echo esc_attr(get_option('site_qq')); ?>"
class="regular-text" placeholder="例如:123456789">
<p class="description">留空则不显示该行。</p>
</td>
</tr>
<tr>
<th scope="row"><label for="site_copyright">版权信息</label></th>
<td>
<input type="text" name="site_copyright" id="site_copyright"
value="<?php echo esc_attr(get_option('site_copyright')); ?>"
class="regular-text" placeholder="例如:© 2026 小辰导航">
<p class="description">留空则自动显示「© 当前年份 小辰导航」。</p>
</td>
</tr>
<tr>
<th scope="row"><label for="site_icp">ICP 备案号</label></th>
<td>
<input type="text" name="site_icp" id="site_icp"
value="<?php echo esc_attr(get_option('site_icp')); ?>"
class="regular-text" placeholder="例如:京ICP备12345678号-1">
<p class="description">留空则不显示;填写后显示在版权下方,可点击跳转工信部备案系统。</p>
</td>
</tr>
</table>
</div>
<?php submit_button('保存侧边栏设置'); ?>
</form>
</div>
<script>
(function () {
var box = document.getElementById('naviModulePills');
if (!box) { return; }
function sync(pill) {
var cb = pill.querySelector('input[type=checkbox]');
var state = pill.querySelector('.navi-pill-state');
var on = cb.checked;
pill.classList.toggle('is-on', on);
if (state) { state.textContent = on ? '显示' : '隐藏'; }
}
box.querySelectorAll('.navi-pill').forEach(function (pill) {
var cb = pill.querySelector('input[type=checkbox]');
cb.addEventListener('change', function () { sync(pill); });
});
document.querySelectorAll('[data-navi-all]').forEach(function (btn) {
btn.addEventListener('click', function () {
var on = btn.getAttribute('data-navi-all') === '1';
box.querySelectorAll('.navi-pill').forEach(function (pill) {
var cb = pill.querySelector('input[type=checkbox]');
cb.checked = on;
sync(pill);
});
});
});
})();
</script>
<?php
}
function navi_render_check_page() {
navi_admin_style();
$query = new WP_Query(array(
'post_type' => 'nav_link',
'post_status' => array('publish', 'pending', 'draft'),
'posts_per_page' => -1,
'orderby' => 'title',
'order' => 'ASC',
'no_found_rows' => true,
));
$links = array();
while ($query->have_posts()) {
$p = $query->next_post();
$url = trim((string) get_post_meta($p->ID, 'link_url', true));
$links[] = array(
'id' => (int) $p->ID,
'title' => get_the_title($p),
'url' => $url,
'edit' => admin_url('post.php?post=' . $p->ID . '&action=edit'),
);
}
wp_reset_postdata();
$rest_root = esc_url_raw(rest_url());
$nonce = wp_create_nonce('wp_rest');
?>
<div class="wrap navi-admin">
<h1>网址检查</h1>
<div class="navi-card">
<h2 class="title">批量检查网址可访问性</h2>
<p class="navi-sub">对每个网址发送 HTTP HEAD 请求(不支持时降级 GET),8 秒超时,最多跟随 5 次重定向。状态码 200–399 视为正常,其他视为失效。</p>
<p class="navi-pill-actions">
<button type="button" class="button button-primary" id="naviCheckStart">开始检查</button>
<button type="button" class="button" id="naviCheckStop" disabled>停止</button>
<button type="button" class="button" id="naviCheckFailOnly">仅看失效</button>
</p>
<div style="margin:12px 0 6px;display:flex;align-items:center;gap:12px;">
<div style="flex:1;height:8px;background:#f0f0f1;border-radius:999px;overflow:hidden;">
<div id="naviCheckBar" style="width:0;height:100%;background:linear-gradient(90deg,#2271b1,#135e96);transition:width .2s;"></div>
</div>
<span id="naviCheckStat" style="font-size:12px;color:#646970;min-width:160px;text-align:right;">未开始</span>
</div>
<table class="wp-list-table widefat striped" id="naviCheckTable" style="margin-top:8px;">
<thead>
<tr>
<th style="width:40px;">#</th>
<th>标题</th>
<th>链接</th>
<th style="width:120px;">状态码</th>
<th style="width:240px;">结果</th>
<th style="width:160px;">操作</th>
</tr>
</thead>
<tbody>
<?php if (empty($links)) : ?>
<tr><td colspan="6">暂无网址,请先在「网址」菜单添加。</td></tr>
<?php else :
$i = 0;
foreach ($links as $link) :
$i++;
$host = $link['url'] ? preg_replace('#^https?://#', '', esc_url($link['url'])) : '';
?>
<tr data-id="<?php echo esc_attr($link['id']); ?>" data-status="pending">
<td><?php echo $i; ?></td>
<td><strong><?php echo esc_html($link['title'] ?: '(无标题)'); ?></strong></td>
<td>
<?php if ($link['url']) : ?>
<a href="<?php echo esc_url($link['url']); ?>" target="_blank" rel="noopener" style="color:#2271b1;"><?php echo esc_html($host); ?></a>
<?php else : ?>
<span style="color:#a7aaad;">未填写</span>
<?php endif; ?>
</td>
<td class="navi-code">—</td>
<td class="navi-result"><span style="color:#a7aaad;">待检查</span></td>
<td class="navi-ops">
<a href="<?php echo esc_url($link['edit']); ?>" class="button button-small">编辑</a>
<button type="button" class="button button-small navi-recheck" disabled>重检</button>
</td>
</tr>
<?php endforeach; endif; ?>
</tbody>
</table>
<p style="margin-top:10px;font-size:12px;color:#646970;" id="naviCheckSummary"></p>
</div>
</div>
<script>
(function () {
var root = '<?php echo $rest_root; ?>nav/v1/check-link';
var nonce = '<?php echo $nonce; ?>';
var table = document.getElementById('naviCheckTable');
if (!table) { return; }
var rows = Array.prototype.slice.call(table.querySelectorAll('tbody tr[data-id]'));
var idx = 0;
var stop = false;
var stats = { ok: 0, fail: 0, empty: 0, total: rows.length };
var startBtn = document.getElementById('naviCheckStart');
var stopBtn = document.getElementById('naviCheckStop');
var failBtn = document.getElementById('naviCheckFailOnly');
var bar = document.getElementById('naviCheckBar');
var stat = document.getElementById('naviCheckStat');
var summary = document.getElementById('naviCheckSummary');
function fmtStat() {
var done = stats.ok + stats.fail + stats.empty;
return done + ' / ' + stats.total + ' 正常 ' + stats.ok + ' 失效 ' + stats.fail + ' 无链接 ' + stats.empty;
}
function setRow(row, data) {
row.dataset.status = data.status;
var c = row.querySelector('.navi-code');
var r = row.querySelector('.navi-result');
var badge = '';
if (data.status === 'ok') {
badge = '<span style="color:#1a8754;font-weight:600;">✓ 正常</span>';
c.innerHTML = '<span style="color:#1a8754;">' + (data.code || '—') + '</span>';
} else if (data.status === 'empty') {
badge = '<span style="color:#a7aaad;">— 未填写</span>';
c.innerHTML = '—';
} else {
badge = '<span style="color:#d63638;font-weight:600;">✗ 失效</span><br><span style="font-size:11px;color:#787c82;">' + (data.message || '').slice(0, 80) + '</span>';
c.innerHTML = '<span style="color:#d63638;">' + (data.code || '0') + '</span>';
row.style.background = '#fef7f7';
}
r.innerHTML = badge;
row.querySelector('.navi-recheck').disabled = false;
}
function checkOne(row) {
var id = row.dataset.id;
row.dataset.status = 'checking';
row.querySelector('.navi-result').innerHTML = '<span style="color:#2271b1;">检查中…</span>';
return fetch(root + '?id=' + id, { headers: { 'X-WP-Nonce': nonce } })
.then(function (r) { return r.json(); })
.then(function (d) { setRow(row, d); stats[d.status]++; })
.catch(function (e) {
setRow(row, { status: 'fail', code: 0, message: '请求异常' });
stats.fail++;
});
}
async function run() {
stop = false;
startBtn.disabled = true;
stopBtn.disabled = false;
idx = 0;
stats = { ok: 0, fail: 0, empty: 0, total: rows.length };
bar.style.width = '0%';
stat.textContent = fmtStat();
summary.textContent = '';
while (idx < rows.length && !stop) {
var row = rows[idx];
if (row.dataset.status === 'pending') {
await checkOne(row);
}
idx++;
bar.style.width = (Math.round((idx / rows.length) * 100)) + '%';
stat.textContent = fmtStat();
}
startBtn.disabled = false;
stopBtn.disabled = true;
if (!stop) {
summary.innerHTML = '检查完成:共 ' + stats.total + ' 条,<strong style="color:#d63638;">失效 ' + stats.fail + '</strong>,<strong style="color:#1a8754;">正常 ' + stats.ok + '</strong>,无链接 ' + stats.empty + '。';
}
}
startBtn.addEventListener('click', run);
stopBtn.addEventListener('click', function () { stop = true; });
failBtn.addEventListener('click', function () {
rows.forEach(function (r) {
r.style.display = (r.dataset.status === 'fail' || r.dataset.status === 'pending') ? '' : 'none';
});
});
table.addEventListener('click', function (e) {
if (e.target.classList.contains('navi-recheck')) {
var tr = e.target.closest('tr[data-id]');
if (tr) {
var status = tr.dataset.status;
if (status === 'fail' || status === 'ok' || status === 'empty') {
stats[status]--;
}
checkOne(tr).then(function () {
stat.textContent = fmtStat();
});
}
}
});
})();
</script>
<?php
}
* 投稿页真实地址(兼容任意固定链接结构,如 /index.php/12/)
*/
function navi_submit_url() {
$page = get_page_by_path('submit');
if ($page) {
return get_permalink($page);
}
return home_url('/submit');
}
* 供模板使用的默认值辅助函数
*/
function navi_get_intro() {
$intro = trim((string) get_option('site_intro'));
if ($intro !== '') {
return $intro;
}
return '小辰导航是一个简约实用的网址导航平台,聚合常用搜索、工具网站、学习资源、影视娱乐、设计素材、开发文档等各类优质网址。界面干净无冗余,分类清晰易查找,致力于成为你打开浏览器的第一个主页,让上网更高效、更省心。';
}
function navi_get_copyright() {
$copyright = trim((string) get_option('site_copyright'));
if ($copyright !== '') {
return $copyright;
}
return '© ' . gmdate('Y') . ' 小辰导航';
}
* 分类图标的 slug 默认值(Bootstrap Icons,MIT 协议可商用)
* 后台“网址分类”中可按分类单独选择图标,选择结果(term meta)优先于此映射
*/
function nav_default_category_icons() {
return array(
'common' => 'bi-globe2',
'ai-tools' => 'bi-robot',
'utility' => 'bi-tools',
'gallery' => 'bi-image',
'entertainment' => 'bi-controller',
'design' => 'bi-palette',
'office' => 'bi-book',
'life' => 'bi-house-heart',
'resources' => 'bi-box-seam',
);
}
* 获取分类图标标识
* 接受 term ID、slug 或 term 对象;返回 bi-* 名称,未设置时取 slug 默认值,兜底文件夹
*/
function get_category_icon($category) {
if (is_object($category)) {
$term_id = isset($category->term_id) ? (int) $category->term_id : 0;
$slug = isset($category->slug) ? $category->slug : '';
} elseif (is_numeric($category)) {
$term_id = (int) $category;
$term = get_term($term_id, 'nav_category');
$slug = ($term && !is_wp_error($term)) ? $term->slug : '';
} else {
$slug = sanitize_title((string) $category);
$term = get_term_by('slug', $slug, 'nav_category');
$term_id = ($term && !is_wp_error($term)) ? (int) $term->term_id : 0;
}
if ($term_id) {
$meta = trim((string) get_term_meta($term_id, 'nav_cat_icon', true));
if ($meta !== '') {
if (strpos($meta, 'bi-') === 0) {
return sanitize_html_class($meta);
}
return $meta;
}
}
$defaults = nav_default_category_icons();
return isset($defaults[$slug]) ? $defaults[$slug] : 'bi-folder';
}
* 把图标标识渲染为 HTML;bi-* 用图标字体,其余(如 emoji)按文本输出
*/
function nav_icon_html($icon, $extra_class = '') {
$icon = trim((string) $icon);
if ($icon === '') {
$icon = 'bi-folder';
}
if (strpos($icon, 'bi-') === 0) {
$class = 'bi ' . $icon . ($extra_class ? ' ' . $extra_class : '');
return '<i class="' . esc_attr($class) . '" aria-hidden="true"></i>';
}
return esc_html($icon);
}
function get_category_order($category_id) {
$orders = array(
'common' => 1,
'ai-tools' => 2,
'utility' => 3,
'gallery' => 4,
'entertainment' => 5,
'design' => 6,
'office' => 7,
'life' => 8,
'resources' => 9
);
return isset($orders[$category_id]) ? $orders[$category_id] : 99;
}
add_action('rest_api_init', function() {
register_rest_field('nav_category', 'icon', array(
'get_callback' => function($object) {
return get_category_icon($object['slug']);
},
'schema' => array(
'type' => 'string',
'description' => '分类图标',
),
));
register_rest_field('nav_category', 'order', array(
'get_callback' => function($object) {
return get_category_order($object['slug']);
},
'schema' => array(
'type' => 'integer',
'description' => '分类排序',
),
));
});
* 主题激活时自动创建“网站投稿”页面
* page-{slug}.php 模板会自动套用到对应 slug 的页面
*/
add_action('after_switch_theme', function() {
if (!get_page_by_path('submit')) {
wp_insert_post(array(
'post_type' => 'page',
'post_status' => 'publish',
'post_title' => '网站投稿',
'post_name' => 'submit',
'post_content'=> '',
));
}
});