feat(pages): 1:1 文档导航 + 文章归档 (zibll documentnav/archives)
Category accordion + AJAX cat/search posts for documentnav; archives month lists with view/comment/like meta, sidebar and comments shell.
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
<?php
|
||||
/**
|
||||
* Document navigation (zibll documentnav) — category tree + AJAX posts.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Core;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class DocumentNav {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
add_action('wp_ajax_aksrt_documentnav_posts', [$this, 'ajax_posts']);
|
||||
add_action('wp_ajax_nopriv_aksrt_documentnav_posts', [$this, 'ajax_posts']);
|
||||
add_action('add_meta_boxes', [$this, 'register_meta_box']);
|
||||
add_action('save_post_page', [$this, 'save_meta_box']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{cat:int,initial_content:string}
|
||||
*/
|
||||
public static function get_options(int $post_id): array {
|
||||
$raw = get_post_meta($post_id, 'documentnav_options', true);
|
||||
if (!is_array($raw)) {
|
||||
$raw = [];
|
||||
}
|
||||
$cat = isset($raw['cat']) ? absint($raw['cat']) : absint(get_post_meta($post_id, 'documentnav_cat', true));
|
||||
$initial = isset($raw['initial_content']) ? sanitize_key((string) $raw['initial_content']) : sanitize_key((string) get_post_meta($post_id, 'documentnav_initial_content', true));
|
||||
if (!in_array($initial, ['page_content', 'date_posts', 'updated_posts', 'views_posts'], true)) {
|
||||
$initial = 'updated_posts';
|
||||
}
|
||||
return [
|
||||
'cat' => $cat,
|
||||
'initial_content' => $initial,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Render posts list HTML (zibll zib_get_documentnav_posts).
|
||||
*/
|
||||
public static function render_posts_html(\WP_Query $query): string {
|
||||
if (!$query->have_posts()) {
|
||||
$empty = function_exists('aksrt_get_null')
|
||||
? aksrt_get_null(__('暂无相应内容', 'aksrt'), 40)
|
||||
: '<p class="muted-2-color text-center">' . esc_html__('暂无相应内容', 'aksrt') . '</p>';
|
||||
return '<div class="zib-widget">' . $empty . '</div>';
|
||||
}
|
||||
|
||||
$html = '<div class="list-group main-shadow">';
|
||||
while ($query->have_posts()) {
|
||||
$query->the_post();
|
||||
$time_ago = human_time_diff(get_the_time('U'), current_time('timestamp')) . __('前', 'aksrt');
|
||||
$subtitle = (string) get_post_meta(get_the_ID(), 'subtitle', true);
|
||||
if ($subtitle === '') {
|
||||
$subtitle = (string) get_post_meta(get_the_ID(), 'aksrt_subtitle', true);
|
||||
}
|
||||
$html .= '<a class="list-group-item" href="' . esc_url(get_permalink()) . '">';
|
||||
$html .= '<span class="badg badg-sm pull-right" title="' . esc_attr(get_the_time('Y-m-d H:i:s')) . '">' . esc_html($time_ago) . '</span>';
|
||||
$html .= esc_html(get_the_title());
|
||||
if ($subtitle !== '') {
|
||||
$html .= '<span class="focus-color"> ' . esc_html($subtitle) . '</span>';
|
||||
}
|
||||
$html .= '</a>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
wp_reset_postdata();
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple paginate links for document nav container.
|
||||
*/
|
||||
public static function render_paginate(int $total, int $current, int $page_size, array $args = []): string {
|
||||
$max = (int) ceil(max(0, $total) / max(1, $page_size));
|
||||
if ($max <= 1) {
|
||||
return '';
|
||||
}
|
||||
$current = max(1, min($current, $max));
|
||||
$html = '<div class="pagenav notop text-center mt15">';
|
||||
for ($i = 1; $i <= $max; $i++) {
|
||||
$url = add_query_arg(array_merge($args, ['paged' => $i]));
|
||||
$cls = $i === $current ? 'current' : '';
|
||||
$html .= '<a class="' . esc_attr($cls) . '" href="' . esc_url($url) . '" data-paged="' . (int) $i . '">' . (int) $i . '</a> ';
|
||||
}
|
||||
$html .= '</div>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive category accordion HTML.
|
||||
*/
|
||||
public static function render_panels(int $cat_id, string $data_parent = 'accordion', string $class = 'panel', bool &$is_on_open = true): string {
|
||||
if ($cat_id <= 0) {
|
||||
return '';
|
||||
}
|
||||
$terms = _get_term_hierarchy('category');
|
||||
if (empty($terms[$cat_id]) || !is_array($terms[$cat_id])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$html = '<div class="panel-group posts-panel-group" id="' . esc_attr($data_parent) . '">';
|
||||
foreach ($terms[$cat_id] as $child) {
|
||||
$child = (int) $child;
|
||||
if ($child === $cat_id) {
|
||||
continue;
|
||||
}
|
||||
$cat_obj = get_category($child);
|
||||
if (!$cat_obj || is_wp_error($cat_obj)) {
|
||||
continue;
|
||||
}
|
||||
$has_kids = !empty($terms[$child]);
|
||||
if ($has_kids) {
|
||||
$open = $is_on_open;
|
||||
$is_on_open = false;
|
||||
$html .= '<div class="' . esc_attr($class) . '">';
|
||||
$html .= '<h4 class="panel-title">';
|
||||
$html .= '<a class="panel-toggle' . ($open ? '' : ' collapsed') . ' text-ellipsis" href="javascript:;" data-toggle="collapse" data-target="#dosc-nav-catid_' . $child . '" data-parent="#' . esc_attr($data_parent) . '">';
|
||||
$html .= '<i class="fa fa-angle-right mr10" aria-hidden="true"></i>' . esc_html($cat_obj->name);
|
||||
$html .= '</a></h4>';
|
||||
$html .= '<div id="dosc-nav-catid_' . $child . '" class="panel-collapse' . ($open ? ' collapse in is-open' : ' collapse') . '">';
|
||||
$html .= '<div class="category">';
|
||||
$html .= self::render_panels($child, 'dosc-cat-nav-panel-' . $child, 'panel panel-child', $is_on_open);
|
||||
$html .= '</div></div></div>';
|
||||
} else {
|
||||
$html .= '<a class="cat-load text-ellipsis" data-cat-id="' . $child . '" href="javascript:;" title="' . esc_attr($cat_obj->name) . '">';
|
||||
$html .= '<i class="loading" aria-hidden="true"></i>' . esc_html($cat_obj->name);
|
||||
$html .= '</a>';
|
||||
}
|
||||
}
|
||||
$html .= '</div>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
public function ajax_posts(): void {
|
||||
$search = isset($_REQUEST['search']) ? sanitize_text_field(wp_unslash((string) $_REQUEST['search'])) : '';
|
||||
$cat_id = isset($_REQUEST['cat_id']) ? absint($_REQUEST['cat_id']) : 0;
|
||||
$one_cat = isset($_REQUEST['one_cat']) ? absint($_REQUEST['one_cat']) : 0;
|
||||
$paged = isset($_REQUEST['paged']) ? max(1, absint($_REQUEST['paged'])) : 1;
|
||||
$page_size = 12;
|
||||
|
||||
if ($search === '' && $cat_id <= 0) {
|
||||
wp_send_json(['error' => 1, 'ys' => 'danger', 'msg' => __('参数传入错误!', 'aksrt')]);
|
||||
}
|
||||
|
||||
$args = [
|
||||
'ignore_sticky_posts' => true,
|
||||
'paged' => $paged,
|
||||
'posts_per_page' => $page_size,
|
||||
'post_status' => 'publish',
|
||||
'post_type' => 'post',
|
||||
];
|
||||
|
||||
if ($search !== '') {
|
||||
$args['s'] = $search;
|
||||
$args['cat'] = $one_cat > 0 ? $one_cat : 0;
|
||||
// Persist simple history (user meta or cookie-less option list is optional).
|
||||
$this->remember_search($search);
|
||||
} else {
|
||||
$args['cat'] = $cat_id;
|
||||
}
|
||||
|
||||
$query = new \WP_Query($args);
|
||||
$html = self::render_posts_html($query);
|
||||
$html .= self::render_paginate(
|
||||
(int) $query->found_posts,
|
||||
$paged,
|
||||
$page_size,
|
||||
[
|
||||
'action' => 'aksrt_documentnav_posts',
|
||||
'cat_id' => $cat_id,
|
||||
'search' => $search,
|
||||
'one_cat' => $one_cat,
|
||||
]
|
||||
);
|
||||
|
||||
wp_send_json([
|
||||
'error' => 0,
|
||||
'html' => $html,
|
||||
'total' => (int) $query->found_posts,
|
||||
'history_search' => $this->history_buttons_html(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function remember_search(string $term): void {
|
||||
$term = trim($term);
|
||||
if ($term === '' || mb_strlen($term) < 2) {
|
||||
return;
|
||||
}
|
||||
$key = is_user_logged_in() ? 'aksrt_doc_search_history_' . get_current_user_id() : '';
|
||||
$list = [];
|
||||
if ($key !== '') {
|
||||
$saved = get_user_meta(get_current_user_id(), 'aksrt_doc_search_history', true);
|
||||
$list = is_array($saved) ? $saved : [];
|
||||
} else {
|
||||
$cookie = isset($_COOKIE['aksrt_doc_search_history']) ? wp_unslash((string) $_COOKIE['aksrt_doc_search_history']) : '';
|
||||
$decoded = json_decode($cookie, true);
|
||||
$list = is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
$list = array_values(array_unique(array_filter(array_map('strval', $list))));
|
||||
array_unshift($list, $term);
|
||||
$list = array_slice(array_values(array_unique($list)), 0, 8);
|
||||
if ($key !== '') {
|
||||
update_user_meta(get_current_user_id(), 'aksrt_doc_search_history', $list);
|
||||
} elseif (!headers_sent()) {
|
||||
setcookie('aksrt_doc_search_history', wp_json_encode($list), time() + WEEK_IN_SECONDS, COOKIEPATH ?: '/', COOKIE_DOMAIN, is_ssl(), true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function get_history(): array {
|
||||
if (is_user_logged_in()) {
|
||||
$saved = get_user_meta(get_current_user_id(), 'aksrt_doc_search_history', true);
|
||||
return is_array($saved) ? array_values(array_filter(array_map('strval', $saved))) : [];
|
||||
}
|
||||
$cookie = isset($_COOKIE['aksrt_doc_search_history']) ? wp_unslash((string) $_COOKIE['aksrt_doc_search_history']) : '';
|
||||
$decoded = json_decode($cookie, true);
|
||||
return is_array($decoded) ? array_values(array_filter(array_map('strval', $decoded))) : [];
|
||||
}
|
||||
|
||||
public function history_buttons_html(): string {
|
||||
$items = $this->get_history();
|
||||
if (!$items) {
|
||||
return '';
|
||||
}
|
||||
$html = '';
|
||||
foreach ($items as $term) {
|
||||
$html .= '<a href="javascript:;" class="search_keywords but badg mr6 mb6">' . esc_html($term) . '</a>';
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
||||
public function register_meta_box(): void {
|
||||
add_meta_box(
|
||||
'aksrt_documentnav_meta',
|
||||
__('文档导航设置', 'aksrt'),
|
||||
[$this, 'render_meta_box'],
|
||||
'page',
|
||||
'side',
|
||||
'default'
|
||||
);
|
||||
}
|
||||
|
||||
public function render_meta_box(\WP_Post $post): void {
|
||||
$tpl = (string) get_post_meta($post->ID, '_wp_page_template', true);
|
||||
if ($tpl !== 'templates/pages/page-documentnav.php') {
|
||||
echo '<p class="description">' . esc_html__('将模板设为「AKSRT - 文档导航」后配置。', 'aksrt') . '</p>';
|
||||
return;
|
||||
}
|
||||
$opt = self::get_options((int) $post->ID);
|
||||
wp_nonce_field('aksrt_documentnav_meta', 'aksrt_documentnav_meta_nonce');
|
||||
$cats = get_categories(['hide_empty' => false, 'parent' => 0]);
|
||||
?>
|
||||
<p>
|
||||
<label for="documentnav_cat"><strong><?php esc_html_e('一级分类', 'aksrt'); ?></strong></label>
|
||||
<select name="documentnav_cat" id="documentnav_cat" class="widefat">
|
||||
<option value="0"><?php esc_html_e('请选择', 'aksrt'); ?></option>
|
||||
<?php foreach ($cats as $c) : ?>
|
||||
<option value="<?php echo (int) $c->term_id; ?>" <?php selected($opt['cat'], (int) $c->term_id); ?>>
|
||||
<?php echo esc_html($c->name); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
<label for="documentnav_initial_content"><strong><?php esc_html_e('初始内容', 'aksrt'); ?></strong></label>
|
||||
<select name="documentnav_initial_content" id="documentnav_initial_content" class="widefat">
|
||||
<?php
|
||||
$opts = [
|
||||
'page_content' => __('显示页面内容', 'aksrt'),
|
||||
'date_posts' => __('最近发布文章', 'aksrt'),
|
||||
'updated_posts' => __('最近更新文章', 'aksrt'),
|
||||
'views_posts' => __('查看最多文章', 'aksrt'),
|
||||
];
|
||||
foreach ($opts as $k => $label) :
|
||||
?>
|
||||
<option value="<?php echo esc_attr($k); ?>" <?php selected($opt['initial_content'], $k); ?>><?php echo esc_html($label); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function save_meta_box(int $post_id): void {
|
||||
if (!isset($_POST['aksrt_documentnav_meta_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash((string) $_POST['aksrt_documentnav_meta_nonce'])), 'aksrt_documentnav_meta')) {
|
||||
return;
|
||||
}
|
||||
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
|
||||
return;
|
||||
}
|
||||
if (!current_user_can('edit_post', $post_id)) {
|
||||
return;
|
||||
}
|
||||
$cat = isset($_POST['documentnav_cat']) ? absint($_POST['documentnav_cat']) : 0;
|
||||
$initial = isset($_POST['documentnav_initial_content']) ? sanitize_key((string) wp_unslash($_POST['documentnav_initial_content'])) : 'updated_posts';
|
||||
if (!in_array($initial, ['page_content', 'date_posts', 'updated_posts', 'views_posts'], true)) {
|
||||
$initial = 'updated_posts';
|
||||
}
|
||||
update_post_meta($post_id, 'documentnav_options', [
|
||||
'cat' => $cat,
|
||||
'initial_content' => $initial,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -458,6 +458,42 @@ trait ScriptsPart1b {
|
||||
}
|
||||
}
|
||||
|
||||
// 文章归档页
|
||||
if (is_page_template('templates/pages/page-archives.php')) {
|
||||
$ar_css = get_template_directory() . '/assets/css/routes/archives.css';
|
||||
if (file_exists($ar_css)) {
|
||||
wp_enqueue_style(
|
||||
'aksrt-route-archives',
|
||||
$base_uri . '/assets/css/routes/archives.css',
|
||||
['aksrt-tokens'],
|
||||
(string) filemtime($ar_css)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 文档导航页(zibll documentnav.php)
|
||||
if (is_page_template('templates/pages/page-documentnav.php')) {
|
||||
$dn_css = get_template_directory() . '/assets/css/routes/documentnav.css';
|
||||
if (file_exists($dn_css)) {
|
||||
wp_enqueue_style(
|
||||
'aksrt-route-documentnav',
|
||||
$base_uri . '/assets/css/routes/documentnav.css',
|
||||
['aksrt-tokens'],
|
||||
(string) filemtime($dn_css)
|
||||
);
|
||||
}
|
||||
$dn_js = get_template_directory() . '/assets/js/documentnav-page.js';
|
||||
if (file_exists($dn_js)) {
|
||||
wp_enqueue_script(
|
||||
'aksrt-documentnav-page',
|
||||
$base_uri . '/assets/js/documentnav-page.js',
|
||||
[],
|
||||
(string) filemtime($dn_js),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 用户身份认证页(zibll user-auth.php)
|
||||
if (is_page_template('templates/pages/page-user-auth.php')) {
|
||||
$ua_css = get_template_directory() . '/assets/css/routes/user-auth.css';
|
||||
|
||||
@@ -40,6 +40,7 @@ class Templates {
|
||||
'templates/pages/page-profile.php' => __('AKSRT - 个人中心', 'aksrt'),
|
||||
'templates/pages/page-user-home.php' => __('AKSRT - 用户主页', 'aksrt'),
|
||||
'templates/pages/page-archives.php' => __('AKSRT - 文章归档', 'aksrt'),
|
||||
'templates/pages/page-documentnav.php' => __('AKSRT - 文档导航', 'aksrt'),
|
||||
'templates/pages/page-categories.php' => __('AKSRT - 分类汇总', 'aksrt'),
|
||||
'templates/pages/page-newposts.php' => __('AKSRT - 最新文章', 'aksrt'),
|
||||
// 注意:page-newposts = 最新文章列表;投稿页是 page-submit(写文章/投稿)
|
||||
|
||||
Reference in New Issue
Block a user