diff --git a/assets/css/routes/posts-navs.css b/assets/css/routes/posts-navs.css new file mode 100644 index 00000000..fc601cb8 --- /dev/null +++ b/assets/css/routes/posts-navs.css @@ -0,0 +1,261 @@ +/** + * AKSRT 文章导航页 — zibll postsnavs shell + admin settings. + * + * @package AKSRT + */ + +/* header card */ +.page-template-posts-navs .content-wrap, +main.page-template-posts-navs { + display: flex; + flex-wrap: wrap; + gap: 20px; +} + +main.page-template-posts-navs .content-wrap { + flex: 1 1 0; + min-width: 0; +} + +main.page-template-posts-navs .sidebar { + flex: 0 0 260px; + max-width: 100%; +} + +.posts-navs .title-theme h2 { + margin: 0; + font-size: 1.15em; +} + +.posts-navs__list { + list-style: none; + margin: 0; + padding: 0; +} + +.posts-navs__item { + margin: 0 0 10px; + padding: 0; +} + +.posts-navs__item .text-ellipsis { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.posts-navs__item .icon-circle { + display: inline-block; + width: 4px; + height: 4px; + border-radius: 50%; + background: var(--muted-3-color, #ccc); + margin: 0 0.55em; + vertical-align: middle; +} + +.posts-navs__item a { + color: var(--color-ink, inherit); + text-decoration: none; +} + +.posts-navs__item a:hover { + color: var(--focus-color, var(--color-primary)); +} + +.posts-navs .theme-box { + margin-bottom: 20px; +} + +.posts-navs .box-body.notop { + padding-top: 0; +} + +/* TOC sidebar filled by section-navs.js */ +.posts-nav-box .posts-nav-lists, +.posts-nav-box .aksrt-section-nav { + max-height: 400px; + padding-left: 10px; + overflow-y: auto; +} + +.posts-nav-box .zib-widget { + background: var(--main-bg-color, var(--surface-1, #fff)); + border-radius: 8px; + box-shadow: 0 1px 4px var(--main-shadow, rgba(0, 0, 0, 0.06)); + padding: 10px; +} + +.posts-nav-lists .bl { + border-left: 1px solid var(--main-border-color, #eee); + padding: 12px 10px 12px 0; + list-style: none; + margin: 0; + position: relative; +} + +.posts-nav-lists li { + position: relative; + list-style: none; +} + +.posts-nav-lists li a { + margin: 0 10px; + display: block; + border-radius: 4px; + padding: 8px; + margin-right: 20px; + color: var(--muted-2-color, #666); + text-decoration: none; +} + +.posts-nav-lists li a:hover, +.posts-nav-lists li.active a { + background: var(--main-shadow, rgba(0, 0, 0, 0.04)); + color: var(--focus-color, var(--color-primary)); +} + +.posts-nav-lists li.n-H1 a, +.posts-nav-lists li.n-H2 a { + font-weight: 700; +} + +/* settings modal */ +#post-navs-sz.modal { + position: fixed; + inset: 0; + z-index: 1050; + display: none; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.45); + padding: 16px; +} + +#post-navs-sz.modal.is-open, +#post-navs-sz.modal.show { + display: flex; +} + +#post-navs-sz .modal-dialog { + width: min(640px, 100%); + max-height: 90vh; + overflow: auto; +} + +#post-navs-sz .modal-content { + background: var(--main-bg-color, #fff); + border-radius: 10px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.18); +} + +#post-navs-sz .modal-header, +#post-navs-sz .modal-body, +#post-navs-sz .modal-footer { + padding: 16px 20px; +} + +#post-navs-sz .modal-header { + border-bottom: 1px solid var(--main-border-color, #eee); +} + +#post-navs-sz .modal-footer { + border-top: 1px solid var(--main-border-color, #eee); + text-align: right; +} + +#post-navs-sz .close { + float: right; + border: 0; + background: transparent; + cursor: pointer; +} + +#post-navs-sz .form-control { + width: 100%; + padding: 8px 10px; + border: 1px solid var(--main-border-color, #ddd); + border-radius: 6px; + box-sizing: border-box; +} + +#post-navs-sz .form-group { + margin-bottom: 14px; +} + +#post-navs-sz .row { + display: flex; + flex-wrap: wrap; + margin: 0 -8px; +} + +#post-navs-sz .col-xs-6, +#post-navs-sz .col-sm-6 { + width: 50%; + padding: 0 8px; + box-sizing: border-box; +} + +.cat-group { + height: 200px; + background: var(--body-bg-color, var(--surface-2, #f6f7f9)); + border: 1px solid var(--main-border-color, #ddd); + border-radius: 4px; + overflow: hidden; + padding: 5px; + overflow-y: auto; +} + +.sortable-cat-item { + background: var(--main-border-color, #e8e8e8); + padding: 4px 8px; + transition: 0.2s; + border-radius: 4px; + margin: 2px; + display: inline-block; + cursor: grab; + width: calc(100% - 15px); + min-width: 100px; + box-sizing: border-box; + user-select: none; +} + +.sortable-cat-item .badge { + margin-left: 4px; + background: rgba(0, 0, 0, 0.08); + color: inherit; + border-radius: 10px; + padding: 0 6px; + font-size: 12px; +} + +#cat-sortable-ok .sortable-cat-item { + background: rgba(73, 214, 253, 0.12); + color: #069cf3; +} + +.sortable-cat-item.is-dragging { + opacity: 0.5; +} + +.sortable-cat-item.sortable-selected { + background: rgba(251, 82, 82, 0.1) !important; + color: #fb5454 !important; +} + +.cat-group.is-drop-target { + outline: 2px dashed var(--focus-color, #f04494); +} + +@media (max-width: 991px) { + main.page-template-posts-navs .sidebar { + flex: 1 1 100%; + order: -1; + } + + #post-navs-sz .col-xs-6, + #post-navs-sz .col-sm-6 { + width: 100%; + margin-bottom: 12px; + } +} diff --git a/assets/js/posts-navs-page.js b/assets/js/posts-navs-page.js new file mode 100644 index 00000000..aa81a3fc --- /dev/null +++ b/assets/js/posts-navs-page.js @@ -0,0 +1,255 @@ +/** + * AKSRT 文章导航页 — admin settings modal + HTML5 drag-sort (no SortableJS dependency). + * TOC generation is handled by section-navs.js via [data-nav] + .posts-nav-box. + * + * @package AKSRT + */ +(function () { + 'use strict'; + + function qs(sel, root) { + return (root || document).querySelector(sel); + } + + function qsa(sel, root) { + return Array.prototype.slice.call((root || document).querySelectorAll(sel)); + } + + function ajaxUrl() { + if (window.aksrtData && window.aksrtData.ajaxUrl) return window.aksrtData.ajaxUrl; + if (window.ajaxurl) return window.ajaxurl; + return '/wp-admin/admin-ajax.php'; + } + + function toast(msg, type) { + if (window.aksrt && typeof window.aksrt.toast === 'function') { + window.aksrt.toast(msg, type || 'info'); + return; + } + // eslint-disable-next-line no-alert + window.alert(msg); + } + + function initModal() { + var modal = qs('#post-navs-sz'); + if (!modal) return; + + function openModal(e) { + if (e) e.preventDefault(); + modal.classList.add('is-open', 'show'); + modal.setAttribute('aria-hidden', 'false'); + } + + function closeModal(e) { + if (e) e.preventDefault(); + modal.classList.remove('is-open', 'show'); + modal.setAttribute('aria-hidden', 'true'); + } + + qsa('[data-toggle="modal"][data-target="#post-navs-sz"], a[href="#post-navs-sz"]').forEach(function (el) { + el.addEventListener('click', openModal); + }); + // Title [设置] uses data-target. + qsa('[data-target="#post-navs-sz"]').forEach(function (el) { + el.addEventListener('click', openModal); + }); + qsa('[data-dismiss="modal"]', modal).forEach(function (btn) { + btn.addEventListener('click', closeModal); + }); + modal.addEventListener('click', function (e) { + if (e.target === modal) closeModal(e); + }); + document.addEventListener('keydown', function (e) { + if (e.key === 'Escape' && modal.classList.contains('is-open')) closeModal(e); + }); + } + + function initDragSort() { + var left = qs('#cat-sortable'); + var right = qs('#cat-sortable-ok'); + if (!left || !right) return; + + var dragEl = null; + + function onDragStart(e) { + dragEl = e.currentTarget; + dragEl.classList.add('is-dragging'); + try { + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', dragEl.getAttribute('data-id') || ''); + } catch (err) { /* IE */ } + } + + function onDragEnd() { + if (dragEl) dragEl.classList.remove('is-dragging'); + dragEl = null; + qsa('.cat-group').forEach(function (g) { + g.classList.remove('is-drop-target'); + }); + } + + function bindItem(el) { + el.setAttribute('draggable', 'true'); + el.addEventListener('dragstart', onDragStart); + el.addEventListener('dragend', onDragEnd); + } + + qsa('.sortable-cat-item', left).forEach(bindItem); + qsa('.sortable-cat-item', right).forEach(bindItem); + + [left, right].forEach(function (group) { + group.addEventListener('dragover', function (e) { + e.preventDefault(); + group.classList.add('is-drop-target'); + if (!dragEl) return; + var after = getDragAfterElement(group, e.clientY); + if (after == null) { + group.appendChild(dragEl); + } else { + group.insertBefore(dragEl, after); + } + }); + group.addEventListener('dragleave', function () { + group.classList.remove('is-drop-target'); + }); + group.addEventListener('drop', function (e) { + e.preventDefault(); + group.classList.remove('is-drop-target'); + }); + }); + + function getDragAfterElement(container, y) { + var els = qsa('.sortable-cat-item:not(.is-dragging)', container); + var closest = { offset: Number.NEGATIVE_INFINITY, element: null }; + els.forEach(function (child) { + var box = child.getBoundingClientRect(); + var offset = y - box.top - box.height / 2; + if (offset < 0 && offset > closest.offset) { + closest = { offset: offset, element: child }; + } + }); + return closest.element; + } + } + + function initSave() { + var form = qs('#aksrt-posts-navs-settings'); + if (!form) return; + + form.addEventListener('submit', function (e) { + e.preventDefault(); + var ids = []; + qsa('#cat-sortable-ok .sortable-cat-item').forEach(function (item) { + var id = item.getAttribute('data-id'); + if (id) ids.push(id); + }); + if (!ids.length) { + toast('请选择需要显示的分类', 'danger'); + return; + } + + var fd = new FormData(form); + fd.set('action', 'aksrt_post_navs_settings'); + // PHP accepts array; append each. + fd.delete('navs_show_cat_id'); + ids.forEach(function (id) { + fd.append('navs_show_cat_id[]', id); + }); + + var btn = qs('button[type="submit"]', form); + if (btn) btn.disabled = true; + + fetch(ajaxUrl(), { + method: 'POST', + body: fd, + credentials: 'same-origin', + }) + .then(function (r) { return r.json(); }) + .then(function (data) { + if (!data || data.error) { + toast((data && data.msg) || '保存失败', 'danger'); + return; + } + toast(data.msg || '保存成功', 'success'); + if (data.reload) { + window.setTimeout(function () { window.location.reload(); }, 400); + } + }) + .catch(function () { + toast('网络错误', 'danger'); + }) + .finally(function () { + if (btn) btn.disabled = false; + }); + }); + } + + /** + * Build simple TOC into .posts-nav-box if section-navs hasn't already. + * Uses category h2 headings inside .posts-navs. + */ + function initFallbackToc() { + var box = qs('.posts-nav-box'); + var root = qs('.posts-navs[data-nav]'); + if (!box || !root) return; + if (box.querySelector('.posts-nav-lists, .aksrt-section-nav, .toc-list')) return; + + var headings = qsa('.title-theme h2, h2', root); + if (headings.length < 1) return; + + var title = box.getAttribute('data-title') || '导航目录'; + var list = ''; + headings.forEach(function (h, i) { + if (!h.id) h.id = 'navs-sec-' + i; + list += '
  • ' + (h.textContent || '').trim() + '
  • '; + }); + + box.innerHTML = + '
    ' + title + '
    ' + + '
    ' + + '
    '; + + qsa('a[href^="#"]', box).forEach(function (a) { + a.addEventListener('click', function (e) { + var id = a.getAttribute('href'); + var el = id ? document.getElementById(id.replace(/^#/, '')) : null; + if (!el) return; + e.preventDefault(); + var top = el.getBoundingClientRect().top + window.pageYOffset - 90; + window.scrollTo({ top: top, behavior: 'smooth' }); + }); + }); + + function highlight() { + var y = window.pageYOffset + 120; + var current = headings[0]; + headings.forEach(function (h) { + if (h.offsetTop <= y) current = h; + }); + qsa('li', box).forEach(function (li) { + var a = li.querySelector('a'); + if (a && current && a.getAttribute('href') === '#' + current.id) { + li.classList.add('active'); + } else { + li.classList.remove('active'); + } + }); + } + window.addEventListener('scroll', highlight, { passive: true }); + highlight(); + } + + function init() { + initModal(); + initDragSort(); + initSave(); + // Defer so section-navs.js can own TOC first. + window.setTimeout(initFallbackToc, 50); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); diff --git a/functions.php b/functions.php index f7cce329..4a31e905 100644 --- a/functions.php +++ b/functions.php @@ -255,6 +255,9 @@ if (class_exists('\AKSRT\Core\LinksSubmit')) { if (class_exists('\AKSRT\Core\LinksPageMeta')) { \AKSRT\Core\LinksPageMeta::get_instance()->init(); } +if (class_exists('\AKSRT\Core\PostsNavs')) { + \AKSRT\Core\PostsNavs::get_instance()->init(); +} if (class_exists('\AKSRT\Messages\SiteNotice')) { \AKSRT\Messages\SiteNotice::get_instance()->init(); } diff --git a/src/AKSRT/Core/PostsNavs.php b/src/AKSRT/Core/PostsNavs.php new file mode 100644 index 00000000..4021fb78 --- /dev/null +++ b/src/AKSRT/Core/PostsNavs.php @@ -0,0 +1,77 @@ + 1, 'ys' => 'danger', 'msg' => __('权限不足', 'aksrt')]); + } + if (!check_ajax_referer('aksrt_post_navs_settings', '_wpnonce', false)) { + wp_send_json(['error' => 1, 'ys' => 'danger', 'msg' => __('安全校验失败', 'aksrt')]); + } + + $page_id = isset($_POST['page_id']) ? absint($_POST['page_id']) : 0; + if ($page_id <= 0 || get_post_type($page_id) !== 'page') { + wp_send_json(['error' => 1, 'ys' => 'danger', 'msg' => __('无效页面', 'aksrt')]); + } + + $cat_ids = []; + if (isset($_POST['navs_show_cat_id'])) { + $raw = wp_unslash($_POST['navs_show_cat_id']); + if (is_array($raw)) { + $cat_ids = array_values(array_filter(array_map('intval', $raw))); + } elseif (is_string($raw) && $raw !== '') { + $decoded = json_decode($raw, true); + if (is_array($decoded)) { + $cat_ids = array_values(array_filter(array_map('intval', $decoded))); + } else { + $cat_ids = array_values(array_filter(array_map('intval', explode(',', $raw)))); + } + } + } + + if (empty($cat_ids)) { + wp_send_json(['error' => 1, 'ys' => 'danger', 'msg' => __('请选择需要显示的分类', 'aksrt')]); + } + + if (isset($_POST['navs_page_name'])) { + update_post_meta($page_id, 'navs_page_name', sanitize_text_field(wp_unslash((string) $_POST['navs_page_name']))); + } + if (isset($_POST['navs_page_desc'])) { + update_post_meta($page_id, 'navs_page_desc', sanitize_text_field(wp_unslash((string) $_POST['navs_page_desc']))); + } + update_post_meta($page_id, 'navs_show_cat_id', $cat_ids); + + wp_send_json([ + 'error' => 0, + 'msg' => __('保存成功', 'aksrt'), + 'reload'=> true, + 'ids' => $cat_ids, + ]); + } +} diff --git a/src/AKSRT/Core/ScriptsPart1b.php b/src/AKSRT/Core/ScriptsPart1b.php index 6791cb17..eb17842a 100644 --- a/src/AKSRT/Core/ScriptsPart1b.php +++ b/src/AKSRT/Core/ScriptsPart1b.php @@ -404,6 +404,39 @@ trait ScriptsPart1b { } } + // 文章导航页(zibll postsnavs.php) + if (is_page_template('templates/pages/page-posts-navs.php')) { + $pn_css = get_template_directory() . '/assets/css/routes/posts-navs.css'; + if (file_exists($pn_css)) { + wp_enqueue_style( + 'aksrt-route-posts-navs', + $base_uri . '/assets/css/routes/posts-navs.css', + ['aksrt-tokens'], + (string) filemtime($pn_css) + ); + } + $section_navs = get_template_directory() . '/assets/js/section-navs.js'; + if (file_exists($section_navs)) { + wp_enqueue_script( + 'aksrt-section-navs', + $base_uri . '/assets/js/section-navs.js', + ['jquery'], + (string) filemtime($section_navs), + true + ); + } + $pn_js = get_template_directory() . '/assets/js/posts-navs-page.js'; + if (file_exists($pn_js)) { + wp_enqueue_script( + 'aksrt-posts-navs-page', + $base_uri . '/assets/js/posts-navs-page.js', + ['jquery'], + (string) filemtime($pn_js), + true + ); + } + } + // 交易中心共享壳层(购物车 / 订单 / 收益) $is_commerce_center = is_page_template('templates/pages/page-orders.php') || is_page_template('templates/pages/page-cart.php') diff --git a/templates/pages/page-posts-navs.php b/templates/pages/page-posts-navs.php index 28e84ebe..2d371d28 100644 --- a/templates/pages/page-posts-navs.php +++ b/templates/pages/page-posts-navs.php @@ -1,65 +1,228 @@ ID : (int) get_queried_object_id(); -$categories = get_categories(['hide_empty' => true, 'orderby' => 'count', 'order' => 'DESC', 'number' => 20]); -$tags = get_tags(['hide_empty' => true, 'orderby' => 'count', 'order' => 'DESC', 'number' => 40]); - -if (function_exists('aksrt_archive_open_shell')) { - aksrt_archive_open_shell([ - 'main_class' => 'site-main container content-shell content-shell--page page-content-default', - 'has_sidebar' => false, - 'page_kind' => 'posts-navs', - ]); +$show_cat_id = get_post_meta($post_id, 'navs_show_cat_id', true); +if (!is_array($show_cat_id)) { + $show_cat_id = $show_cat_id !== '' && $show_cat_id !== null + ? array_values(array_filter(array_map('intval', (array) $show_cat_id))) + : []; +} else { + $show_cat_id = array_values(array_filter(array_map('intval', $show_cat_id))); } + +$page_desc = (string) get_post_meta($post_id, 'navs_page_desc', true); +$page_name = (string) get_post_meta($post_id, 'navs_page_name', true); +$title = get_the_title($post_id); +$page_name = $page_name !== '' ? $page_name : $title; + +$all_cats = get_categories([ + 'orderby' => 'term_group', + 'order' => 'ASC', + 'hide_empty' => false, +]); + +$can_admin = is_user_logged_in() && current_user_can('manage_options'); + +get_header(); ?> -
    - esc_html(get_the_title() ?: __('文章导航', 'aksrt')), + + +
    +
    +
    +

    + + + + + [] + + + +

    +
    +
    + +
    +
    + true]); + } elseif (function_exists('aksrt_get_share_html')) { + echo aksrt_get_share_html(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + ?> +
    +
    +
    +
    +
    + +
    +
    +
    +
    + (int) $c_id, + 'numberposts' => -1, + 'post_status' => 'publish', + 'orderby' => 'date', + 'order' => 'DESC', + 'suppress_filters' => false, ]); - ?> + if (empty($posts)) { + continue; + } + $section_id = 'navs-cat-' . (int) $c_id; + echo '
    '; + echo '

    ' . esc_html($the_cat->name) . '

    '; + echo '
      '; + foreach ($posts as $p) { + $time_tip = get_the_time('Y年m月d日 H:i', $p); + $time_ago = human_time_diff(get_the_time('U', $p), current_time('timestamp')) . __('前', 'aksrt'); + $views = function_exists('aksrt_get_post_views') ? (int) aksrt_get_post_views($p->ID) : 0; + $likes = (int) get_post_meta($p->ID, 'like', true); + if ($likes <= 0) { + $likes = (int) get_post_meta($p->ID, 'aksrt_like', true); + } + $post_tip = sprintf( + /* translators: 1: views 2: likes */ + __('阅读(%1$s) 点赞(%2$s)', 'aksrt'), + (string) $views, + (string) $likes + ); + $subtitle = (string) get_post_meta($p->ID, 'subtitle', true); + if ($subtitle === '') { + $subtitle = (string) get_post_meta($p->ID, 'aksrt_subtitle', true); + } + echo '
    • '; + echo '
    • '; + } + echo '
    '; + } + } elseif ($can_admin) { + echo '
    '; + echo '

    ' . esc_html__('尚未选择分类,请点击标题旁「设置」配置要展示的分类。', 'aksrt') . '

    '; + echo '
    '; + } else { + echo '
    '; + echo '

    ' . esc_html__('暂无导航内容', 'aksrt') . '

    '; + echo '
    '; + } + ?> +
    +
    +
    + +
    -
    - -
    -

    -
    - - - name); ?> - count); ?> - - + +
    - - - -
    -

    -
    - - name); ?> - +
    +
    +

    +
    + '), '>'); + echo '' + . esc_html($c_name) + . '' . (int) $the_cat->count . ''; + } + ?>
    -
    - +
    +
    + + + + + + false]); -} +endif; get_footer();