feat(forum): detail/mini topic list item 1:1 structure

Expand render_topic_row into zibll-like detail cards (author card,
cover, excerpt, multi-image grid, vote strip, plate + action-meta)
and keep mini compact variant for dense lists.
This commit is contained in:
2026-07-18 16:47:30 +08:00
parent 433b7cb3a6
commit 770fa83332
3 changed files with 281 additions and 39 deletions
+98
View File
@@ -586,6 +586,104 @@
text-shadow: 0 1px 2px rgba(0,0,0,.35);
}
/* detail list card (zibll forum-posts detail) */
.aksrt-forum-topic-row.detail,
.aksrt-forum-posts.detail {
display: block;
padding: 16px;
margin-bottom: 12px;
border-radius: 14px;
background: var(--surface-1, #fff);
box-shadow: var(--shadow-sm, 0 1px 4px rgba(0,0,0,.06));
}
.aksrt-forum-topic-row__author-card { gap: 10px; }
.aksrt-forum-topic-row__author-name { font-weight: 700; font-size: 14px; }
.aksrt-forum-topic-row.detail .aksrt-forum-topic-row__cover,
.aksrt-forum-posts.detail .forum-lists-cover {
display: block;
width: 100%;
height: auto;
max-height: 280px;
margin: 0 0 12px;
border-radius: 12px;
overflow: hidden;
}
.aksrt-forum-topic-row.detail .aksrt-forum-topic-row__cover img {
width: 100%;
height: auto;
max-height: 280px;
object-fit: cover;
display: block;
}
.aksrt-forum-topic-row .excerpt {
margin-top: 6px;
font-size: 14px;
line-height: 1.65;
color: var(--color-ink-2, #4b5563);
}
.aksrt-forum-topic-row .lists-imgs {
display: grid;
gap: 6px;
margin-top: 8px;
}
.aksrt-forum-topic-row .lists-imgs.count-1 { grid-template-columns: 1fr; max-width: 280px; }
.aksrt-forum-topic-row .lists-imgs.count-2 { grid-template-columns: 1fr 1fr; }
.aksrt-forum-topic-row .lists-imgs.count-3,
.aksrt-forum-topic-row .lists-imgs.count-4 { grid-template-columns: 1fr 1fr; }
.aksrt-forum-topic-row .lists-imgs__item {
display: block;
border-radius: 10px;
overflow: hidden;
aspect-ratio: 1;
background: var(--surface-2, #f3f4f6);
}
.aksrt-forum-topic-row .lists-imgs__item img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.aksrt-forum-topic-row__vote { display: grid; gap: 6px; }
.aksrt-forum-topic-row__vote-opt {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(80px, 2fr) 42px;
gap: 8px;
align-items: center;
font-size: 13px;
}
.aksrt-forum-topic-row__vote-bar {
height: 8px;
border-radius: 999px;
background: var(--surface-4, #eef0f3);
overflow: hidden;
}
.aksrt-forum-topic-row__vote-bar i {
display: block;
height: 100%;
background: var(--color-primary, #f04494);
}
.aksrt-forum-topic-row__footer .but-plate,
.aksrt-forum-topic-row__board.but {
display: inline-flex;
align-items: center;
max-width: 46%;
padding: 4px 10px;
border-radius: 999px;
background: var(--surface-4, #f3f4f6);
color: var(--color-ink-2, #5c6370);
text-decoration: none;
font-size: 12px;
font-weight: 600;
}
.aksrt-forum-topic-row__actions {
display: inline-flex;
gap: 12px;
color: var(--color-ink-3, #8a909c);
font-size: 13px;
}
.aksrt-forum-topic-row__actions a { color: inherit; text-decoration: none; }
.aksrt-forum-topic-row.mini { display: flex; gap: 12px; align-items: flex-start; }
.aksrt-forum-board-card__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
+2 -2
View File
@@ -250,8 +250,8 @@ class Forum {
$this->view_builder()->render_board_cards($boards);
}
public function render_topic_row(array $topic): void {
$this->view_builder()->render_topic_row($topic);
public function render_topic_row(array $topic, string $style = 'detail'): void {
$this->view_builder()->render_topic_row($topic, $style);
}
public function get_board_view(int $board_id, array $args = []): array {
+181 -37
View File
@@ -435,58 +435,97 @@ class ViewBuilder {
/**
* Render a single topic row (reusable for AJAX and initial render).
*
* @param array $topic Topic payload.
* @param string $style detail|mini (zibll forum-posts parity).
*/
public function render_topic_row(array $topic): void {
public function render_topic_row(array $topic, string $style = 'detail'): void {
$style = $style === 'mini' ? 'mini' : 'detail';
$board = !empty($topic['boards'][0]) ? $topic['boards'][0] : null;
$names = $this->forum->get_config()['names'] ?? [];
$sticky_level = (int) ($topic['sticky_level'] ?? (!empty($topic['sticky']) ? 1 : 0));
$row_class = 'aksrt-forum-topic-row';
$permalink = (string) ($topic['permalink'] ?? '#');
$cover = is_array($topic['cover'] ?? null) ? $topic['cover'] : null;
$vote = is_array($topic['vote'] ?? null) ? $topic['vote'] : null;
$excerpt = trim((string) ($topic['excerpt'] ?? ''));
$topic_cat = is_array($topic['topic_cat'] ?? null) ? $topic['topic_cat'] : null;
$row_class = 'aksrt-forum-topic-row aksrt-forum-posts forum-posts ajax-item ' . $style;
if ($sticky_level > 0) {
$row_class .= ' is-sticky is-sticky-' . $sticky_level;
}
if (!empty($topic['featured'])) {
$row_class .= ' is-featured';
}
$cover = $topic['cover'] ?? null;
$badges = '';
if ($sticky_level > 0) {
$badges .= '<span class="aksrt-forum-topic-row__badge aksrt-forum-topic-row__badge--sticky badge-topping">'
. esc_html((string) ($topic['sticky_label'] ?? __('置顶', 'aksrt'))) . '</span>';
}
if (!empty($topic['featured'])) {
$badges .= '<span class="aksrt-forum-topic-row__badge aksrt-forum-topic-row__badge--featured badge-essence">'
. esc_html__('精华', 'aksrt') . '</span>';
}
if (($topic['bbs_type'] ?? '') === 'question') {
$q_status = (int) ($topic['question_status'] ?? 0);
$badges .= '<span class="aksrt-forum-topic-row__badge aksrt-forum-topic-row__badge--question'
. ($q_status === 1 ? ' is-solved' : '') . '">'
. esc_html($q_status === 1
? (string) ($names['question_ok_badge'] ?? __('已解决', 'aksrt'))
: (string) ($names['question_badge'] ?? __('提问', 'aksrt')))
. '</span>';
}
$title_html = $badges
. '<h3 class="aksrt-forum-topic-row__title forum-title">'
. '<a href="' . esc_url($permalink) . '">' . esc_html((string) ($topic['title'] ?? '')) . '</a></h3>';
if ($style === 'mini') {
?>
<article class="<?php echo esc_attr($row_class); ?>" data-topic-id="<?php echo esc_attr((string) ($topic['id'] ?? 0)); ?>">
<div class="aksrt-forum-topic-row__avatar forum-user">
<?php if (!empty($topic['author']['avatar_url'])) : ?>
<img src="<?php echo esc_url((string) $topic['author']['avatar_url']); ?>" alt="" width="40" height="40" loading="lazy" class="forum-avatar">
<?php endif; ?>
</div>
<div class="aksrt-forum-topic-row__body entry-info">
<div class="aksrt-forum-topic-row__title-wrap"><?php echo $title_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
<div class="aksrt-forum-topic-row__meta item-meta flex ac jsb">
<div class="meta-left">
<span class="aksrt-forum-topic-row__author"><?php echo esc_html((string) ($topic['author']['display_name'] ?? '')); ?></span>
<span class="aksrt-forum-topic-row__time icon-circle"><?php echo esc_html((string) ($topic['date'] ?? '')); ?></span>
</div>
<div class="meta-right action-meta">
<span title="<?php esc_attr_e('浏览', 'aksrt'); ?>"><i class="fas fa-eye"></i> <?php echo esc_html(number_format_i18n((int) ($topic['views'] ?? 0))); ?></span>
<span title="<?php esc_attr_e('回复', 'aksrt'); ?>"><i class="fas fa-comment"></i> <?php echo esc_html(number_format_i18n((int) ($topic['comment_count'] ?? 0))); ?></span>
<span title="<?php esc_attr_e('评分', 'aksrt'); ?>"><i class="fas fa-thumbs-up"></i> <?php echo esc_html(number_format_i18n((int) ($topic['score'] ?? 0))); ?></span>
</div>
</div>
</div>
</article>
<?php
return;
}
// detail
$list_imgs = $this->extract_topic_list_images($topic, 4);
?>
<article class="<?php echo esc_attr($row_class); ?>">
<div class="aksrt-forum-topic-row__avatar">
<?php if (!empty($topic['author']['avatar_url'])) : ?>
<img src="<?php echo esc_url((string) $topic['author']['avatar_url']); ?>" alt="" width="40" height="40" loading="lazy">
<?php endif; ?>
</div>
<div class="aksrt-forum-topic-row__body">
<div class="aksrt-forum-topic-row__title-wrap">
<?php if ($sticky_level > 0) : ?>
<span class="aksrt-forum-topic-row__badge aksrt-forum-topic-row__badge--sticky"><?php echo esc_html((string) ($topic['sticky_label'] ?? __('置顶', 'aksrt'))); ?></span>
<article class="<?php echo esc_attr($row_class); ?>" data-topic-id="<?php echo esc_attr((string) ($topic['id'] ?? 0)); ?>">
<div class="aksrt-forum-topic-row__author-card mb10 flex ac">
<div class="aksrt-forum-topic-row__avatar">
<?php if (!empty($topic['author']['avatar_url'])) : ?>
<img src="<?php echo esc_url((string) $topic['author']['avatar_url']); ?>" alt="" width="44" height="44" loading="lazy">
<?php endif; ?>
<?php if (!empty($topic['featured'])) : ?>
<span class="aksrt-forum-topic-row__badge aksrt-forum-topic-row__badge--featured"><?php esc_html_e('精华', 'aksrt'); ?></span>
<?php endif; ?>
<?php if (($topic['bbs_type'] ?? '') === 'question') : ?>
<?php $q_status = (int) ($topic['question_status'] ?? 0); ?>
<span class="aksrt-forum-topic-row__badge aksrt-forum-topic-row__badge--question<?php echo $q_status === 1 ? ' is-solved' : ''; ?>">
<?php echo $q_status === 1
? esc_html($names['question_ok_badge'] ?? __('已解决', 'aksrt'))
: esc_html($names['question_badge'] ?? __('提问', 'aksrt')); ?>
</span>
<?php endif; ?>
<h3 class="aksrt-forum-topic-row__title">
<a href="<?php echo esc_url((string) ($topic['permalink'] ?? '#')); ?>"><?php echo esc_html((string) ($topic['title'] ?? '')); ?></a>
</h3>
</div>
<div class="aksrt-forum-topic-row__meta">
<?php if ($board) : ?>
<a class="aksrt-forum-topic-row__board" href="<?php echo esc_url((string) ($board['url'] ?? '#')); ?>"><?php echo esc_html((string) ($board['name'] ?? '')); ?></a>
<?php endif; ?>
<span class="aksrt-forum-topic-row__author"><?php echo esc_html((string) ($topic['author']['display_name'] ?? '')); ?></span>
<span class="aksrt-forum-topic-row__time"><?php echo esc_html((string) ($topic['date'] ?? '')); ?></span>
<span><i class="fas fa-eye"></i> <?php echo esc_html(number_format_i18n((int) ($topic['views'] ?? 0))); ?></span>
<span><i class="fas fa-comment"></i> <?php echo esc_html(number_format_i18n((int) ($topic['comment_count'] ?? 0))); ?></span>
<div class="aksrt-forum-topic-row__author-meta">
<div class="aksrt-forum-topic-row__author-name"><?php echo esc_html((string) ($topic['author']['display_name'] ?? '')); ?></div>
<div class="aksrt-forum-topic-row__time muted-2-color em09"><?php echo esc_html((string) ($topic['date'] ?? '')); ?></div>
</div>
</div>
<?php if (is_array($cover) && !empty($cover['type'])) : ?>
<div class="aksrt-forum-topic-row__cover">
<a class="aksrt-forum-topic-row__cover forum-lists-cover" href="<?php echo esc_url($permalink); ?>">
<?php if (($cover['type'] ?? '') === 'video') : ?>
<?php if (!empty($cover['poster'])) : ?>
<img src="<?php echo esc_url((string) $cover['poster']); ?>" alt="" loading="lazy">
@@ -498,12 +537,117 @@ class ViewBuilder {
<?php elseif (!empty($cover['url'])) : ?>
<img src="<?php echo esc_url((string) $cover['url']); ?>" alt="" loading="lazy">
<?php endif; ?>
</div>
</a>
<?php endif; ?>
<div class="aksrt-forum-topic-row__body">
<div class="aksrt-forum-topic-row__title-wrap"><?php echo $title_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
<div class="content mt6">
<?php if ($topic_cat && !empty($topic_cat['name'])) : ?>
<a class="aksrt-forum-topic-row__topic-link focus-color mr3" href="<?php echo esc_url((string) ($topic_cat['url'] ?? '#')); ?>">
<i class="fas fa-hashtag"></i><?php echo esc_html((string) $topic_cat['name']); ?>
</a>
<?php endif; ?>
<?php if ($excerpt !== '') : ?>
<div class="excerpt"><?php echo esc_html($excerpt); ?></div>
<?php endif; ?>
</div>
<?php if (!empty($list_imgs) && empty($cover)) : ?>
<div class="imgbox-container lists-imgs mt6 count-<?php echo esc_attr((string) count($list_imgs)); ?>">
<?php foreach ($list_imgs as $img_url) : ?>
<a class="lists-imgs__item cursor-zoom-in" href="<?php echo esc_url($permalink); ?>">
<img src="<?php echo esc_url($img_url); ?>" alt="" loading="lazy">
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!empty($vote['enabled']) && !empty($vote['options']) && is_array($vote['options'])) : ?>
<div class="aksrt-forum-topic-row__vote mt10">
<?php
$total_votes = 0;
foreach ($vote['options'] as $opt) {
$total_votes += (int) ($opt['count'] ?? 0);
}
foreach (array_slice($vote['options'], 0, 4) as $opt) :
$cnt = (int) ($opt['count'] ?? 0);
$pct = $total_votes > 0 ? round($cnt * 100 / $total_votes) : 0;
?>
<div class="aksrt-forum-topic-row__vote-opt">
<span class="aksrt-forum-topic-row__vote-label"><?php echo esc_html((string) ($opt['label'] ?? $opt['text'] ?? '')); ?></span>
<span class="aksrt-forum-topic-row__vote-bar"><i style="width:<?php echo esc_attr((string) $pct); ?>%"></i></span>
<span class="aksrt-forum-topic-row__vote-pct"><?php echo esc_html((string) $pct); ?>%</span>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="flex ac jsb mt10 aksrt-forum-topic-row__footer">
<?php if ($board) : ?>
<a class="but but-plate aksrt-forum-topic-row__board" href="<?php echo esc_url((string) ($board['url'] ?? '#')); ?>">
<?php echo esc_html((string) ($board['name'] ?? '')); ?>
<i class="fa fa-angle-right ml6"></i>
</a>
<?php else : ?>
<span class="plate-null"></span>
<?php endif; ?>
<div class="action-meta flex0 flex jsb aksrt-forum-topic-row__actions">
<span title="<?php esc_attr_e('评分', 'aksrt'); ?>"><i class="fas fa-thumbs-up"></i> <?php echo esc_html(number_format_i18n((int) ($topic['score'] ?? 0))); ?></span>
<a href="<?php echo esc_url($permalink); ?>#comments" title="<?php esc_attr_e('回复', 'aksrt'); ?>"><i class="fas fa-comment"></i> <?php echo esc_html(number_format_i18n((int) ($topic['comment_count'] ?? 0))); ?></a>
<span title="<?php esc_attr_e('浏览', 'aksrt'); ?>"><i class="fas fa-eye"></i> <?php echo esc_html(number_format_i18n((int) ($topic['views'] ?? 0))); ?></span>
<span title="<?php esc_attr_e('收藏', 'aksrt'); ?>"><i class="fas fa-star"></i> <?php echo esc_html(number_format_i18n((int) ($topic['favorite_count'] ?? 0))); ?></span>
</div>
</div>
</div>
</article>
<?php
}
/**
* Pull up to N image URLs from cover slides or content HTML for list grids.
*
* @return array<int,string>
*/
private function extract_topic_list_images(array $topic, int $max = 4): array {
$max = max(1, min(6, $max));
$out = [];
$cover = is_array($topic['cover'] ?? null) ? $topic['cover'] : null;
if ($cover) {
if (($cover['type'] ?? '') === 'slide' && !empty($cover['images']) && is_array($cover['images'])) {
foreach ($cover['images'] as $u) {
$u = esc_url_raw((string) $u);
if ($u !== '') {
$out[] = $u;
}
if (count($out) >= $max) {
return $out;
}
}
} elseif (!empty($cover['url'])) {
$out[] = esc_url_raw((string) $cover['url']);
}
}
if (count($out) >= $max) {
return $out;
}
$content = (string) ($topic['content'] ?? '');
if ($content !== '' && preg_match_all('/<img[^>]+src=["\']([^"\']+)["\']/i', $content, $m)) {
foreach ($m[1] as $src) {
$src = esc_url_raw((string) $src);
if ($src === '' || in_array($src, $out, true)) {
continue;
}
$out[] = $src;
if (count($out) >= $max) {
break;
}
}
}
return $out;
}
public function get_user_forum_topics(int $user_id, array $args = []): array {
$status = $args['status'] ?? 'publish';
$page = max(1, (int) ($args['page'] ?? 1));