feat(forum): multi-tab topic widget lazy REST load
SSR first tab; other tabs fetch /forum/frontend/widgets/topics on switch, with shared list markup and styles.
This commit is contained in:
1 parent
5cb6928b1e
commit
d479723814
2 files changed
+142
-88
No files matched your search
@@ -718,3 +718,61 @@
|
|||||||
.aksrt-badge-modal__card { padding: 12px 8px 8px; }
|
.aksrt-badge-modal__card { padding: 12px 8px 8px; }
|
||||||
.aksrt-badge-modal__actions { margin-top: 12px; }
|
.aksrt-badge-modal__actions { margin-top: 12px; }
|
||||||
.aksrt-author-hero__badges { margin-top: 8px; }
|
.aksrt-author-hero__badges { margin-top: 8px; }
|
||||||
|
|
||||||
|
/* Forum tab topics (batch6) */
|
||||||
|
.aksrt-ftab__nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0 0 10px;
|
||||||
|
padding: 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.aksrt-ftab__btn {
|
||||||
|
border: 0;
|
||||||
|
background: var(--surface-2, #f3f4f6);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
.aksrt-ftab__btn.is-active {
|
||||||
|
background: var(--color-primary, #3b82f6);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.aksrt-ftab__panel { display: none; }
|
||||||
|
.aksrt-ftab__panel.is-active { display: block; }
|
||||||
|
.aksrt-ftab__empty { font-size: 13px; color: var(--muted-2-color, #999); padding: 8px 0; }
|
||||||
|
.aksrt-ftopic-list { list-style: none; margin: 0; padding: 0; }
|
||||||
|
.aksrt-ftopic-list__item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 7px 0;
|
||||||
|
border-bottom: 1px dashed var(--color-line, #eee);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.aksrt-ftopic-list__item:last-child { border-bottom: 0; }
|
||||||
|
.aksrt-ftopic-list__link {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.aksrt-ftopic-list__views { color: var(--muted-2-color, #999); font-size: 12px; flex-shrink: 0; }
|
||||||
|
.aksrt-ftopic-list__meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--muted-2-color, #999);
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.aksrt-ftopic-list.is-detail .aksrt-ftopic-list__item {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Multi-tab forum topic lists (zibll zib_bbs_widget_ui_tab_posts lite).
|
* Multi-tab forum topic lists (zibll zib_bbs_widget_ui_tab_posts lite + lazy REST).
|
||||||
*
|
*
|
||||||
* @package AKSRT
|
* @package AKSRT
|
||||||
*/
|
*/
|
||||||
@@ -16,7 +16,7 @@ class WidgetForumTabTopics extends \WP_Widget {
|
|||||||
'aksrt_forum_tab_topics',
|
'aksrt_forum_tab_topics',
|
||||||
esc_html__('论坛 - 多栏目帖子', 'aksrt'),
|
esc_html__('论坛 - 多栏目帖子', 'aksrt'),
|
||||||
[
|
[
|
||||||
'description' => esc_html__('热门/最新/精华等栏目切换的帖子列表。', 'aksrt'),
|
'description' => esc_html__('热门/最新/精华等栏目切换的帖子列表(懒加载)。', 'aksrt'),
|
||||||
'classname' => 'widget_aksrt_forum_tab_topics',
|
'classname' => 'widget_aksrt_forum_tab_topics',
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -36,6 +36,7 @@ class WidgetForumTabTopics extends \WP_Widget {
|
|||||||
}
|
}
|
||||||
$count = max(1, min(20, (int) ($instance['count'] ?? 6)));
|
$count = max(1, min(20, (int) ($instance['count'] ?? 6)));
|
||||||
$uid = 'aksrt-ftab-' . esc_attr($this->id);
|
$uid = 'aksrt-ftab-' . esc_attr($this->id);
|
||||||
|
$rest = rest_url('aksrt/v1/forum/frontend/widgets/topics');
|
||||||
|
|
||||||
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
|
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
|
||||||
@@ -43,32 +44,60 @@ class WidgetForumTabTopics extends \WP_Widget {
|
|||||||
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
}
|
}
|
||||||
|
|
||||||
echo '<div class="aksrt-ftab" id="' . $uid . '">';
|
echo '<div class="aksrt-ftab" id="' . $uid . '" data-rest="' . esc_url($rest) . '" data-count="' . esc_attr((string) $count) . '">';
|
||||||
echo '<ul class="aksrt-ftab__nav" role="tablist">';
|
echo '<ul class="aksrt-ftab__nav" role="tablist">';
|
||||||
foreach ($tabs as $i => $tab) {
|
foreach ($tabs as $i => $tab) {
|
||||||
$cls = $i === 0 ? ' is-active' : '';
|
$cls = $i === 0 ? ' is-active' : '';
|
||||||
echo '<li><button type="button" class="aksrt-ftab__btn' . esc_attr($cls) . '" data-index="' . (int) $i . '">' . esc_html($tab['title']) . '</button></li>';
|
echo '<li><button type="button" class="aksrt-ftab__btn' . esc_attr($cls) . '" data-index="' . (int) $i . '" data-orderby="' . esc_attr($tab['orderby']) . '">' . esc_html($tab['title']) . '</button></li>';
|
||||||
}
|
}
|
||||||
echo '</ul>';
|
echo '</ul>';
|
||||||
|
|
||||||
foreach ($tabs as $i => $tab) {
|
foreach ($tabs as $i => $tab) {
|
||||||
$cls = $i === 0 ? ' is-active' : '';
|
$cls = $i === 0 ? ' is-active' : '';
|
||||||
echo '<div class="aksrt-ftab__panel' . esc_attr($cls) . '" data-index="' . (int) $i . '">';
|
$loaded = $i === 0 ? '1' : '0';
|
||||||
$this->render_list($tab, $count);
|
echo '<div class="aksrt-ftab__panel' . esc_attr($cls) . '" data-index="' . (int) $i . '" data-loaded="' . esc_attr($loaded) . '">';
|
||||||
|
if ($i === 0) {
|
||||||
|
$payload = WidgetForumTopicLists::query_payload([
|
||||||
|
'count' => $count,
|
||||||
|
'orderby' => $tab['orderby'],
|
||||||
|
'style' => 'mini',
|
||||||
|
'paginate' => 'none',
|
||||||
|
], 1);
|
||||||
|
echo $payload['html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
} else {
|
||||||
|
echo '<div class="post_ajax_loader" data-aksrt-ajax-loader></div>';
|
||||||
|
}
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
}
|
}
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
|
|
||||||
?>
|
?>
|
||||||
<script>
|
<script>
|
||||||
(function(){
|
(function(){
|
||||||
var root=document.getElementById(<?php echo wp_json_encode($uid); ?>);
|
var root=document.getElementById(<?php echo wp_json_encode($uid); ?>);
|
||||||
if(!root||root._aksrtFtab)return; root._aksrtFtab=1;
|
if(!root||root._aksrtFtab)return; root._aksrtFtab=1;
|
||||||
|
var rest=root.getAttribute('data-rest')||'';
|
||||||
|
var count=root.getAttribute('data-count')||'6';
|
||||||
var btns=[].slice.call(root.querySelectorAll('.aksrt-ftab__btn'));
|
var btns=[].slice.call(root.querySelectorAll('.aksrt-ftab__btn'));
|
||||||
var panels=[].slice.call(root.querySelectorAll('.aksrt-ftab__panel'));
|
var panels=[].slice.call(root.querySelectorAll('.aksrt-ftab__panel'));
|
||||||
|
function load(panel, orderby){
|
||||||
|
if(!panel||panel.getAttribute('data-loaded')==='1'||!rest)return;
|
||||||
|
var qs=new URLSearchParams({page:'1',count:count,orderby:orderby||'date',style:'mini',paginate:'none'});
|
||||||
|
var url=rest+(rest.indexOf('?')>-1?'&':'?')+qs.toString();
|
||||||
|
panel.classList.add('is-ajax-loading');
|
||||||
|
fetch(url,{credentials:'same-origin',headers:{Accept:'application/json','X-WP-Nonce':(window.aksrtData&&aksrtData.restNonce)||''}})
|
||||||
|
.then(function(r){return r.json()}).then(function(j){
|
||||||
|
var d=(j&&j.data)||j||{};
|
||||||
|
var html=d.html||'';
|
||||||
|
panel.innerHTML=html||'<p class="aksrt-ftab__empty">暂无内容</p>';
|
||||||
|
panel.setAttribute('data-loaded','1');
|
||||||
|
panel.classList.remove('is-ajax-loading');
|
||||||
|
}).catch(function(){panel.classList.remove('is-ajax-loading');});
|
||||||
|
}
|
||||||
function go(i){
|
function go(i){
|
||||||
btns.forEach(function(b,idx){b.classList.toggle('is-active',idx===i);});
|
btns.forEach(function(b,idx){b.classList.toggle('is-active',idx===i);});
|
||||||
panels.forEach(function(p,idx){p.classList.toggle('is-active',idx===i);});
|
panels.forEach(function(p,idx){p.classList.toggle('is-active',idx===i);});
|
||||||
|
var btn=btns[i], panel=panels[i];
|
||||||
|
if(btn&&panel) load(panel, btn.getAttribute('data-orderby')||'date');
|
||||||
}
|
}
|
||||||
btns.forEach(function(b){ b.addEventListener('click', function(){ go(parseInt(b.getAttribute('data-index')||'0',10)); }); });
|
btns.forEach(function(b){ b.addEventListener('click', function(){ go(parseInt(b.getAttribute('data-index')||'0',10)); }); });
|
||||||
})();
|
})();
|
||||||
@@ -78,105 +107,72 @@ class WidgetForumTabTopics extends \WP_Widget {
|
|||||||
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array{title:string,orderby:string} $tab
|
|
||||||
*/
|
|
||||||
private function render_list(array $tab, int $count): void {
|
|
||||||
$query = [
|
|
||||||
'post_type' => 'aksrt_forum_topic',
|
|
||||||
'post_status' => 'publish',
|
|
||||||
'posts_per_page' => $count,
|
|
||||||
'order' => 'DESC',
|
|
||||||
];
|
|
||||||
switch ($tab['orderby']) {
|
|
||||||
case 'views':
|
|
||||||
$query['meta_key'] = '_aksrt_views';
|
|
||||||
$query['orderby'] = 'meta_value_num';
|
|
||||||
break;
|
|
||||||
case 'comments':
|
|
||||||
$query['orderby'] = 'comment_count';
|
|
||||||
break;
|
|
||||||
case 'essence':
|
|
||||||
$query['meta_query'] = [[
|
|
||||||
'key' => '_aksrt_essence',
|
|
||||||
'value' => '1',
|
|
||||||
]];
|
|
||||||
$query['orderby'] = 'date';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$query['orderby'] = 'date';
|
|
||||||
}
|
|
||||||
$q = new \WP_Query($query);
|
|
||||||
if (!$q->have_posts()) {
|
|
||||||
echo '<p class="aksrt-ftab__empty">' . esc_html__('暂无内容', 'aksrt') . '</p>';
|
|
||||||
wp_reset_postdata();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
echo '<ul class="aksrt-ftopic-list is-mini">';
|
|
||||||
while ($q->have_posts()) {
|
|
||||||
$q->the_post();
|
|
||||||
$views = (int) get_post_meta(get_the_ID(), '_aksrt_views', true);
|
|
||||||
echo '<li class="aksrt-ftopic-list__item">';
|
|
||||||
echo '<a class="aksrt-ftopic-list__link" href="' . esc_url(get_permalink()) . '">' . esc_html(get_the_title()) . '</a>';
|
|
||||||
echo '<span class="aksrt-ftopic-list__views">' . esc_html((string) $views) . '</span>';
|
|
||||||
echo '</li>';
|
|
||||||
}
|
|
||||||
echo '</ul>';
|
|
||||||
wp_reset_postdata();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mixed $raw
|
* @param mixed $raw
|
||||||
* @return array<int, array{title:string,orderby:string}>
|
* @return list<array{title:string,orderby:string}>
|
||||||
*/
|
*/
|
||||||
private function normalize_tabs($raw): array {
|
private function normalize_tabs($raw): array {
|
||||||
if (is_string($raw)) {
|
|
||||||
$decoded = json_decode($raw, true);
|
|
||||||
$raw = is_array($decoded) ? $decoded : [];
|
|
||||||
}
|
|
||||||
if (!is_array($raw)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
$out = [];
|
$out = [];
|
||||||
foreach ($raw as $row) {
|
if (is_string($raw)) {
|
||||||
if (!is_array($row)) {
|
foreach (array_filter(explode("\n", $raw)) as $line) {
|
||||||
continue;
|
$parts = array_map('trim', explode('|', $line));
|
||||||
|
if ($parts[0] === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$out[] = [
|
||||||
|
'title' => $parts[0],
|
||||||
|
'orderby' => sanitize_key($parts[1] ?? 'date'),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
$title = sanitize_text_field((string) ($row['title'] ?? ''));
|
return $out;
|
||||||
if ($title === '') {
|
}
|
||||||
continue;
|
if (is_array($raw)) {
|
||||||
}
|
foreach ($raw as $tab) {
|
||||||
$orderby = sanitize_key((string) ($row['orderby'] ?? 'date'));
|
if (!is_array($tab)) {
|
||||||
if (!in_array($orderby, ['date', 'views', 'comments', 'essence'], true)) {
|
continue;
|
||||||
$orderby = 'date';
|
}
|
||||||
}
|
$title = trim((string) ($tab['title'] ?? ''));
|
||||||
$out[] = ['title' => $title, 'orderby' => $orderby];
|
if ($title === '') {
|
||||||
if (count($out) >= 6) {
|
continue;
|
||||||
break;
|
}
|
||||||
|
$out[] = [
|
||||||
|
'title' => $title,
|
||||||
|
'orderby' => sanitize_key((string) ($tab['orderby'] ?? 'date')),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function form($instance): void {
|
public function form($instance): void {
|
||||||
$tabs = is_string($instance['tabs'] ?? null)
|
$defaults = [
|
||||||
? (string) $instance['tabs']
|
'title' => __('论坛栏目', 'aksrt'),
|
||||||
: wp_json_encode($this->normalize_tabs($instance['tabs'] ?? []), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
'count' => 6,
|
||||||
|
'tabs' => "最新|date\n热门|views\n精华|essence",
|
||||||
|
];
|
||||||
|
$instance = wp_parse_args($instance, $defaults);
|
||||||
|
$tabs_val = $instance['tabs'];
|
||||||
|
if (is_array($tabs_val)) {
|
||||||
|
$lines = [];
|
||||||
|
foreach ($tabs_val as $t) {
|
||||||
|
$lines[] = ($t['title'] ?? '') . '|' . ($t['orderby'] ?? 'date');
|
||||||
|
}
|
||||||
|
$tabs_val = implode("\n", $lines);
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
<p>
|
<p>
|
||||||
<label><?php esc_html_e('标题', 'aksrt'); ?>
|
<label><?php esc_html_e('标题', 'aksrt'); ?>
|
||||||
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('title')); ?>" value="<?php echo esc_attr((string) ($instance['title'] ?? '')); ?>">
|
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('title')); ?>" value="<?php echo esc_attr((string) $instance['title']); ?>">
|
||||||
</label>
|
</label>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
|
||||||
<label><?php esc_html_e('栏目 JSON', 'aksrt'); ?>
|
|
||||||
<textarea class="widefat" rows="5" name="<?php echo esc_attr($this->get_field_name('tabs')); ?>" placeholder='[{"title":"最新","orderby":"date"},{"title":"热门","orderby":"views"},{"title":"精华","orderby":"essence"}]'><?php echo esc_textarea((string) $tabs); ?></textarea>
|
|
||||||
</label>
|
|
||||||
<small><?php esc_html_e('orderby: date|views|comments|essence', 'aksrt'); ?></small>
|
|
||||||
</p>
|
|
||||||
<p>
|
<p>
|
||||||
<label><?php esc_html_e('每栏数量', 'aksrt'); ?>
|
<label><?php esc_html_e('每栏数量', 'aksrt'); ?>
|
||||||
<input class="tiny-text" type="number" min="1" max="20" name="<?php echo esc_attr($this->get_field_name('count')); ?>" value="<?php echo esc_attr((string) ((int) ($instance['count'] ?? 6))); ?>">
|
<input class="tiny-text" type="number" min="1" max="20" name="<?php echo esc_attr($this->get_field_name('count')); ?>" value="<?php echo esc_attr((string) $instance['count']); ?>">
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label><?php esc_html_e('栏目(每行: 标题|排序 date/views/essence/comments)', 'aksrt'); ?>
|
||||||
|
<textarea class="widefat" rows="4" name="<?php echo esc_attr($this->get_field_name('tabs')); ?>"><?php echo esc_textarea((string) $tabs_val); ?></textarea>
|
||||||
</label>
|
</label>
|
||||||
</p>
|
</p>
|
||||||
<?php
|
<?php
|
||||||
@@ -185,8 +181,8 @@ class WidgetForumTabTopics extends \WP_Widget {
|
|||||||
public function update($new_instance, $old_instance): array {
|
public function update($new_instance, $old_instance): array {
|
||||||
return [
|
return [
|
||||||
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
|
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
|
||||||
'tabs' => $this->normalize_tabs($new_instance['tabs'] ?? []),
|
|
||||||
'count' => max(1, min(20, (int) ($new_instance['count'] ?? 6))),
|
'count' => max(1, min(20, (int) ($new_instance['count'] ?? 6))),
|
||||||
|
'tabs' => sanitize_textarea_field((string) ($new_instance['tabs'] ?? '')),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user