feat(widgets): partners/iframe/combo/text/user + forum plate widgets

Close major zibll widget-more and bbs plate gaps: partners marquee,
super embed, combo info cards, text title, user lists, and plate
list/info/moderator sidebars with batch3 CSS registration.
This commit is contained in:
2026-07-19 00:15:32 +08:00
parent 6a202f3eab
commit 81bf3193a4
11 changed files with 2026 additions and 0 deletions
+532
View File
@@ -0,0 +1,532 @@
/**
* Batch3 widgets: partners scroll, iframe, combo info, text title, user lists, plate widgets.
* @package AKSRT
*/
/* ---- Partners scroll ---- */
.aksrt-partners-scroll-wrap {
margin-bottom: 20px;
}
.aksrt-partners-scroll-wrap.is-bare {
background: transparent;
box-shadow: none;
padding: 0;
}
.aksrt-partners-scroll {
overflow: hidden;
position: relative;
--aksrt-ps-w: 220px;
--aksrt-ps-h: 90px;
--aksrt-ps-radius: 10px;
--aksrt-ps-speed: 3;
}
.aksrt-ps-end-fade::before,
.aksrt-ps-end-fade::after {
content: "";
position: absolute;
top: 0;
bottom: 0;
width: 48px;
z-index: 2;
pointer-events: none;
}
.aksrt-ps-end-fade::before {
left: 0;
background: linear-gradient(90deg, var(--surface-1, #fff), transparent);
}
.aksrt-ps-end-fade::after {
right: 0;
background: linear-gradient(270deg, var(--surface-1, #fff), transparent);
}
.aksrt-ps-row {
overflow: hidden;
margin-bottom: 10px;
}
.aksrt-ps-row:last-child {
margin-bottom: 0;
}
.aksrt-ps-track {
display: flex;
width: max-content;
animation: aksrt-ps-marquee calc(40s / var(--aksrt-ps-speed)) linear infinite;
}
.aksrt-ps-dir-right .aksrt-ps-track {
animation-name: aksrt-ps-marquee-rev;
}
.aksrt-ps-pause-hover:hover .aksrt-ps-track {
animation-play-state: paused;
}
.aksrt-ps-group {
display: flex;
flex-shrink: 0;
align-items: center;
}
.aksrt-ps-item {
display: flex;
align-items: center;
flex-shrink: 0;
width: var(--aksrt-ps-w);
height: var(--aksrt-ps-h);
margin-right: 12px;
border-radius: var(--aksrt-ps-radius);
overflow: hidden;
text-decoration: none;
color: inherit;
box-sizing: border-box;
padding: 8px 12px;
}
.aksrt-ps-card-bg .aksrt-ps-item {
background: var(--surface-2, #f3f4f6);
}
.aksrt-ps-img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
display: block;
}
.aksrt-ps-style-logo .aksrt-ps-item {
justify-content: center;
padding: 10px;
}
.aksrt-ps-card-icon {
width: 42px;
height: 42px;
flex-shrink: 0;
margin-right: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.aksrt-ps-card-icon .aksrt-ps-img {
max-height: 42px;
}
.aksrt-ps-card-text {
min-width: 0;
flex: 1;
display: flex;
flex-direction: column;
}
.aksrt-ps-card-title {
font-weight: 700;
font-size: 13px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.aksrt-ps-card-desc {
font-size: 12px;
color: var(--muted-2-color, #999);
margin-top: 3px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.aksrt-ps-style-chip .aksrt-ps-item {
width: auto;
height: auto;
min-height: 36px;
border-radius: 999px;
padding: 6px 14px;
gap: 6px;
}
.aksrt-ps-chip-icon {
width: 22px;
height: 22px;
display: flex;
align-items: center;
margin-right: 6px;
}
.aksrt-ps-chip-title {
font-size: 13px;
white-space: nowrap;
}
@keyframes aksrt-ps-marquee {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
@keyframes aksrt-ps-marquee-rev {
from { transform: translateX(-50%); }
to { transform: translateX(0); }
}
/* ---- Iframe embed ---- */
.aksrt-iframe-embed {
margin-bottom: 16px;
}
.aksrt-iframe-ratio {
position: relative;
width: 100%;
height: 0;
border-radius: 8px;
overflow: hidden;
background: var(--surface-2, #f3f4f6);
}
.aksrt-iframe-ratio iframe,
.aksrt-iframe-el {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border: 0;
}
/* ---- Combo info cards ---- */
.aksrt-combo-info {
display: grid;
grid-template-columns: repeat(var(--aksrt-zci-pc, 4), minmax(0, 1fr));
gap: 14px;
margin-bottom: 16px;
--aksrt-zci-radius: 12px;
--aksrt-zci-band-h: 70px;
}
@media (max-width: 991.98px) {
.aksrt-combo-info {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 575.98px) {
.aksrt-combo-info {
grid-template-columns: repeat(var(--aksrt-zci-m, 1), minmax(0, 1fr));
}
}
.aksrt-zci-card {
position: relative;
display: flex;
flex-direction: column;
border-radius: var(--aksrt-zci-radius);
background: var(--surface-1, #fff);
border: 1px solid var(--color-line, #e5e7eb);
padding: 18px 16px 16px;
overflow: hidden;
min-height: 100%;
}
.aksrt-combo-info.has-band .aksrt-zci-card::before {
content: "";
position: absolute;
left: 0;
right: 0;
top: 0;
height: 4px;
background: var(--color-primary, #3b82f6);
}
.aksrt-zci-band-blue::before { background: #3b82f6; }
.aksrt-zci-band-cyan::before { background: #06b6d4; }
.aksrt-zci-band-green::before { background: #22c55e; }
.aksrt-zci-band-yellow::before { background: #eab308; }
.aksrt-zci-band-pink::before { background: #ec4899; }
.aksrt-zci-band-purple::before { background: #a855f7; }
.aksrt-zci-icon {
width: 48px;
height: 48px;
margin-bottom: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
color: var(--color-primary, #3b82f6);
}
.aksrt-zci-icon img {
max-width: 48px;
max-height: 48px;
object-fit: contain;
}
.aksrt-zci-title {
font-weight: 700;
font-size: 16px;
margin-bottom: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.aksrt-zci-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 8px;
}
.aksrt-zci-tag {
display: inline-block;
font-size: 12px;
padding: 2px 10px;
border-radius: 999px;
background: var(--surface-2, #f3f4f6);
color: var(--color-ink-3, #666);
}
.aksrt-zci-desc {
font-size: 13px;
color: var(--muted-2-color, #888);
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
flex: 1;
}
.aksrt-zci-cta {
margin-top: 14px;
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 600;
color: var(--color-primary, #3b82f6);
text-decoration: none;
}
.aksrt-zci-cta:hover i {
transform: translateX(3px);
}
.aksrt-zci-cta i {
transition: transform .2s ease;
font-size: 12px;
}
/* ---- Text title ---- */
.aksrt-text-title {
margin: 8px 0 18px;
--aksrt-tt-title-size: 22px;
--aksrt-tt-desc-size: 14px;
}
.aksrt-text-title.align-center { text-align: center; }
.aksrt-text-title.align-left { text-align: left; }
.aksrt-text-title.align-right { text-align: right; }
.aksrt-text-title__main {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: var(--aksrt-tt-title-size);
font-weight: 700;
color: var(--aksrt-tt-title-color, inherit);
line-height: 1.3;
}
.aksrt-text-title.align-center .aksrt-text-title__main {
justify-content: center;
}
.aksrt-text-title__desc {
margin-top: 8px;
font-size: var(--aksrt-tt-desc-size);
color: var(--aksrt-tt-desc-color, var(--muted-2-color, #888));
line-height: 1.5;
}
.aksrt-text-title__icon {
display: inline-flex;
opacity: .85;
}
/* ---- User lists ---- */
.aksrt-user-lists {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px 8px;
text-align: center;
padding: 4px 0 8px;
}
.aksrt-user-lists.is-bare {
padding: 0;
}
@media (max-width: 575.98px) {
.aksrt-user-lists {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.aksrt-user-lists__item {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
text-decoration: none;
color: inherit;
min-width: 0;
}
.aksrt-user-lists__avatar {
width: 48px;
height: 48px;
border-radius: 50%;
object-fit: cover;
background: var(--surface-2, #eee);
}
.aksrt-user-lists__name {
font-size: 12px;
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ---- Forum plate lists ---- */
.aksrt-plate-lists {
display: flex;
flex-direction: column;
gap: 10px;
}
.aksrt-plate-item {
display: flex;
gap: 12px;
align-items: flex-start;
text-decoration: none;
color: inherit;
padding: 6px 0;
border-bottom: 1px dashed var(--color-line, #e5e7eb);
}
.aksrt-plate-item:last-child {
border-bottom: 0;
}
.aksrt-plate-item__thumb {
width: 48px;
height: 48px;
border-radius: 10px;
overflow: hidden;
flex-shrink: 0;
background: var(--surface-2, #f3f4f6);
display: flex;
align-items: center;
justify-content: center;
color: var(--color-primary, #3b82f6);
font-size: 18px;
}
.aksrt-plate-item__thumb img {
width: 100%;
height: 100%;
object-fit: cover;
}
.aksrt-plate-item__body {
min-width: 0;
flex: 1;
}
.aksrt-plate-item__title {
font-weight: 700;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.aksrt-plate-item__desc {
font-size: 12px;
color: var(--muted-2-color, #888);
margin-top: 3px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.aksrt-plate-item__meta {
font-size: 11px;
color: var(--color-ink-4, #999);
margin-top: 4px;
}
.aksrt-plate-cards {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
@media (max-width: 575.98px) {
.aksrt-plate-cards {
grid-template-columns: 1fr;
}
}
.aksrt-plate-card {
display: block;
text-decoration: none;
color: inherit;
border-radius: 10px;
overflow: hidden;
border: 1px solid var(--color-line, #e5e7eb);
background: var(--surface-1, #fff);
}
.aksrt-plate-card__cover {
height: 88px;
background: var(--surface-2, #f3f4f6) center/cover no-repeat;
}
.aksrt-plate-card__title {
font-weight: 700;
font-size: 13px;
padding: 10px 10px 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.aksrt-plate-card__meta {
display: flex;
justify-content: space-between;
font-size: 11px;
color: var(--muted-2-color, #888);
padding: 0 10px 10px;
}
/* ---- Plate info ---- */
.aksrt-plate-info {
border-radius: 10px;
overflow: hidden;
border: 1px solid var(--color-line, #e5e7eb);
background: var(--surface-1, #fff);
margin-bottom: 12px;
}
.aksrt-plate-info__cover {
height: 90px;
background: var(--surface-2, #f3f4f6) center/cover no-repeat;
}
.aksrt-plate-info__body {
padding: 12px 14px 14px;
}
.aksrt-plate-info__title {
display: block;
font-weight: 700;
font-size: 16px;
color: inherit;
text-decoration: none;
margin-bottom: 6px;
}
.aksrt-plate-info__desc {
font-size: 12px;
color: var(--muted-2-color, #888);
line-height: 1.5;
margin-bottom: 10px;
}
.aksrt-plate-info__stats {
display: flex;
gap: 14px;
font-size: 12px;
color: var(--color-ink-3, #666);
}
.aksrt-plate-info__stats i {
margin-right: 4px;
opacity: .75;
}
/* ---- Plate moderators ---- */
.aksrt-plate-mods {
margin-bottom: 12px;
}
.aksrt-plate-mods__head {
margin-bottom: 10px;
}
.aksrt-plate-mods__list {
display: flex;
flex-direction: column;
gap: 8px;
}
.aksrt-plate-mods__item {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
color: inherit;
}
.aksrt-plate-mods__avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.aksrt-plate-mods__name {
flex: 1;
min-width: 0;
font-size: 13px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.aksrt-plate-mods__role {
font-size: 11px;
color: var(--color-primary, #3b82f6);
flex-shrink: 0;
}
+1
View File
@@ -3,3 +3,4 @@
@import url("widgets/part-3.css");
@import url("widgets/part-4.css");
@import url("widget-slider.css");
@import url("widget-batch3.css");
+16
View File
@@ -56,6 +56,13 @@ function aksrt_register_widgets() {
// 幻灯片
register_widget('\AKSRT\Widgets\WidgetSlider');
// zibll more widgets batch
register_widget('\AKSRT\Widgets\WidgetPartnersScroll');
register_widget('\AKSRT\Widgets\WidgetIframe');
register_widget('\AKSRT\Widgets\WidgetComboInfo');
register_widget('\AKSRT\Widgets\WidgetTextTitle');
register_widget('\AKSRT\Widgets\WidgetUserLists');
// Forum widgets
if (class_exists('\AKSRT\Widgets\WidgetForumBoardList')) {
register_widget('\AKSRT\Widgets\WidgetForumBoardList');
@@ -66,6 +73,15 @@ function aksrt_register_widgets() {
if (class_exists('\AKSRT\Widgets\WidgetForumRecentTopics')) {
register_widget('\AKSRT\Widgets\WidgetForumRecentTopics');
}
if (class_exists('\AKSRT\Widgets\WidgetForumPlateLists')) {
register_widget('\AKSRT\Widgets\WidgetForumPlateLists');
}
if (class_exists('\AKSRT\Widgets\WidgetForumPlateInfo')) {
register_widget('\AKSRT\Widgets\WidgetForumPlateInfo');
}
if (class_exists('\AKSRT\Widgets\WidgetForumPlateModerator')) {
register_widget('\AKSRT\Widgets\WidgetForumPlateModerator');
}
// Shop product widgets (§6.9)
register_widget('\AKSRT\Widgets\WidgetProductList');
+238
View File
@@ -0,0 +1,238 @@
<?php
/**
* Combo info cards (zibll zib_widget_ui_combo_info_card).
*
* @package AKSRT
*/
namespace AKSRT\Widgets;
defined('ABSPATH') || exit;
class WidgetComboInfo extends \WP_Widget {
private const BANDS = ['blue', 'cyan', 'green', 'yellow', 'pink', 'purple'];
public function __construct() {
parent::__construct(
'aksrt_combo_info',
esc_html__('AKSRT 复合信息卡片', 'aksrt'),
[
'description' => esc_html__('图标 + 标题 + 标签 + 描述 + CTA 的多列信息卡。', 'aksrt'),
'classname' => 'widget_aksrt_combo_info',
]
);
}
public function widget($args, $instance): void {
$cards = $this->normalize_cards($instance['cards'] ?? []);
if (!$cards) {
return;
}
$pc_row = max(1, min(6, (int) ($instance['pc_row'] ?? 4)));
$m_row = max(1, min(3, (int) ($instance['m_row'] ?? 1)));
$radius = max(0, min(40, (int) ($instance['card_radius'] ?? 12)));
$top_band = !empty($instance['top_band']) || !isset($instance['top_band']);
$band_h = max(20, min(160, (int) ($instance['top_band_height'] ?? 70)));
$cta_default = (string) ($instance['cta_text'] ?? __('立即体验', 'aksrt'));
$go_link = !empty($instance['go_link']);
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
if ($title !== '') {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
$style = sprintf('--aksrt-zci-radius:%dpx;--aksrt-zci-band-h:%dpx;--aksrt-zci-pc:%d;--aksrt-zci-m:%d;', $radius, $band_h, $pc_row, $m_row);
$wrap = 'aksrt-combo-info' . ($top_band ? ' has-band' : '');
echo '<div class="' . esc_attr($wrap) . '" style="' . esc_attr($style) . '">';
$auto_idx = 0;
foreach ($cards as $card) {
$html = $this->render_card($card, $cta_default, $auto_idx, $go_link, $top_band);
if ($html !== '') {
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
}
echo '</div>';
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* @param array<string,mixed> $card
*/
private function render_card(array $card, string $default_cta, int &$auto_idx, bool $go_link, bool $top_band): string {
$title = trim((string) ($card['title'] ?? ''));
$desc = trim((string) ($card['desc'] ?? ''));
if ($title === '' && $desc === '') {
return '';
}
$band = sanitize_key((string) ($card['band_color'] ?? 'auto'));
if ($band === '' || $band === 'auto' || !in_array($band, self::BANDS, true)) {
$band = self::BANDS[$auto_idx % count(self::BANDS)];
$auto_idx++;
}
$icon_html = '';
$icon_img = (string) ($card['icon_image'] ?? '');
$icon_fa = (string) ($card['icon'] ?? '');
if ($icon_img !== '') {
$icon_html = '<div class="aksrt-zci-icon"><img src="' . esc_url($icon_img) . '" alt="" loading="lazy" /></div>';
} elseif ($icon_fa !== '') {
$icon_html = '<div class="aksrt-zci-icon"><i class="' . esc_attr($icon_fa) . '" aria-hidden="true"></i></div>';
}
$tags_html = '';
$tags = $card['tags'] ?? [];
if (is_string($tags)) {
$tags = array_filter(array_map('trim', explode(',', $tags)));
$tags = array_map(static fn($t) => ['text' => $t, 'color' => ''], $tags);
}
if (is_array($tags) && $tags) {
$tags_html .= '<div class="aksrt-zci-tags">';
foreach ($tags as $tag) {
if (is_string($tag)) {
$tag = ['text' => $tag, 'color' => ''];
}
if (!is_array($tag)) {
continue;
}
$tt = trim((string) ($tag['text'] ?? ''));
if ($tt === '') {
continue;
}
$color = sanitize_html_class((string) ($tag['color'] ?? ''));
$tags_html .= '<span class="aksrt-zci-tag' . ($color !== '' ? ' ' . esc_attr($color) : '') . '">' . esc_html($tt) . '</span>';
}
$tags_html .= '</div>';
}
$cta_text = trim((string) ($card['cta_text'] ?? ''));
if ($cta_text === '') {
$cta_text = $default_cta;
}
$url = (string) ($card['url'] ?? '');
if ($url === '' && is_array($card['link'] ?? null)) {
$url = (string) ($card['link']['url'] ?? '');
}
$target = (string) ($card['target'] ?? '');
if ($target === '' && is_array($card['link'] ?? null)) {
$target = (string) ($card['link']['target'] ?? '');
}
$cta_html = '';
if ($cta_text !== '' && $url !== '') {
if ($go_link && function_exists('aksrt_go_link')) {
$url = aksrt_go_link($url, true);
}
$tgt = $target !== '' ? ' target="' . esc_attr($target) . '" rel="noopener noreferrer"' : '';
$cta_html = '<a class="aksrt-zci-cta" href="' . esc_url($url) . '"' . $tgt . '><span>' . esc_html($cta_text) . '</span><i class="fa fa-arrow-right" aria-hidden="true"></i></a>';
}
$band_class = $top_band ? ' aksrt-zci-band-' . esc_attr($band) : '';
$html = '<div class="aksrt-zci-card' . $band_class . '">';
$html .= $icon_html;
if ($title !== '') {
$html .= '<div class="aksrt-zci-title">' . esc_html($title) . '</div>';
}
$html .= $tags_html;
if ($desc !== '') {
$html .= '<div class="aksrt-zci-desc">' . esc_html($desc) . '</div>';
}
$html .= $cta_html;
$html .= '</div>';
return $html;
}
/**
* @param mixed $raw
* @return array<int, array<string,mixed>>
*/
private function normalize_cards($raw): array {
if (is_string($raw)) {
$decoded = json_decode($raw, true);
$raw = is_array($decoded) ? $decoded : [];
}
if (!is_array($raw)) {
return [];
}
$out = [];
foreach ($raw as $row) {
if (!is_array($row)) {
continue;
}
$title = sanitize_text_field((string) ($row['title'] ?? ''));
$desc = sanitize_text_field((string) ($row['desc'] ?? ''));
if ($title === '' && $desc === '') {
continue;
}
$out[] = [
'title' => $title,
'desc' => $desc,
'icon' => sanitize_text_field((string) ($row['icon'] ?? '')),
'icon_image' => esc_url_raw(trim((string) ($row['icon_image'] ?? ''))),
'url' => esc_url_raw(trim((string) ($row['url'] ?? (is_array($row['link'] ?? null) ? ($row['link']['url'] ?? '') : '')))),
'target' => sanitize_text_field((string) ($row['target'] ?? '_blank')),
'cta_text' => sanitize_text_field((string) ($row['cta_text'] ?? '')),
'band_color' => sanitize_key((string) ($row['band_color'] ?? 'auto')),
'tags' => $row['tags'] ?? [],
];
if (count($out) >= 12) {
break;
}
}
return $out;
}
public function form($instance): void {
$title = (string) ($instance['title'] ?? '');
$cards = is_string($instance['cards'] ?? null)
? (string) $instance['cards']
: wp_json_encode($this->normalize_cards($instance['cards'] ?? []), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
?>
<p>
<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($title); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('卡片 JSON', 'aksrt'); ?>
<textarea class="widefat" rows="8" name="<?php echo esc_attr($this->get_field_name('cards')); ?>" placeholder='[{"title":"功能","desc":"说明","icon":"fas fa-star","url":"https://","cta_text":"了解更多","tags":[{"text":"热门","color":"c-blue"}]}]'><?php echo esc_textarea((string) $cards); ?></textarea>
</label>
</p>
<p>
<label><?php esc_html_e('PC 列数', 'aksrt'); ?>
<input class="tiny-text" type="number" min="1" max="6" name="<?php echo esc_attr($this->get_field_name('pc_row')); ?>" value="<?php echo esc_attr((string) ((int) ($instance['pc_row'] ?? 4))); ?>">
</label>
<label><?php esc_html_e('移动列数', 'aksrt'); ?>
<input class="tiny-text" type="number" min="1" max="3" name="<?php echo esc_attr($this->get_field_name('m_row')); ?>" value="<?php echo esc_attr((string) ((int) ($instance['m_row'] ?? 1))); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('默认 CTA 文案', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('cta_text')); ?>" value="<?php echo esc_attr((string) ($instance['cta_text'] ?? __('立即体验', 'aksrt'))); ?>">
</label>
</p>
<p>
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('top_band')); ?>" value="1" <?php checked(!empty($instance['top_band']) || !isset($instance['top_band'])); ?>> <?php esc_html_e('顶部色带', 'aksrt'); ?></label><br>
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('go_link')); ?>" value="1" <?php checked(!empty($instance['go_link'])); ?>> <?php esc_html_e('外链跳转页', 'aksrt'); ?></label>
</p>
<?php
}
public function update($new_instance, $old_instance): array {
return [
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
'cards' => $this->normalize_cards($new_instance['cards'] ?? []),
'pc_row' => max(1, min(6, (int) ($new_instance['pc_row'] ?? 4))),
'm_row' => max(1, min(3, (int) ($new_instance['m_row'] ?? 1))),
'card_radius' => max(0, min(40, (int) ($new_instance['card_radius'] ?? 12))),
'top_band' => !empty($new_instance['top_band']) ? 1 : 0,
'top_band_height' => max(20, min(160, (int) ($new_instance['top_band_height'] ?? 70))),
'cta_text' => sanitize_text_field((string) ($new_instance['cta_text'] ?? '')),
'go_link' => !empty($new_instance['go_link']) ? 1 : 0,
];
}
}
@@ -0,0 +1,94 @@
<?php
/**
* Current plate info mini-box (zibll zib_bbs_widget_ui_plate_info).
*
* @package AKSRT
*/
namespace AKSRT\Widgets;
defined('ABSPATH') || exit;
class WidgetForumPlateInfo extends \WP_Widget {
public function __construct() {
parent::__construct(
'aksrt_forum_plate_info',
esc_html__('论坛 - 当前版块信息', 'aksrt'),
[
'description' => esc_html__('在版块/帖子页侧栏显示当前 plate 信息。', 'aksrt'),
'classname' => 'widget_aksrt_forum_plate_info',
]
);
}
public function widget($args, $instance): void {
$plate = $this->resolve_current_plate();
if (!$plate) {
return;
}
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$id = (int) $plate->ID;
$url = get_permalink($id);
$thumb = get_the_post_thumbnail_url($id, 'medium');
$posts = (int) get_post_meta($id, '_aksrt_posts_count', true);
$reply = (int) get_post_meta($id, '_aksrt_reply_count', true);
$follow = (int) get_post_meta($id, '_aksrt_follow_count', true);
$desc = $plate->post_excerpt !== '' ? $plate->post_excerpt : wp_trim_words(wp_strip_all_tags($plate->post_content), 30);
echo '<div class="aksrt-plate-info">';
if ($thumb) {
echo '<div class="aksrt-plate-info__cover" style="background-image:url(' . esc_url($thumb) . ')"></div>';
}
echo '<div class="aksrt-plate-info__body">';
echo '<a class="aksrt-plate-info__title" href="' . esc_url($url) . '">' . esc_html(get_the_title($plate)) . '</a>';
if ($desc !== '') {
echo '<div class="aksrt-plate-info__desc">' . esc_html($desc) . '</div>';
}
echo '<div class="aksrt-plate-info__stats">';
echo '<span><i class="fas fa-file-alt" aria-hidden="true"></i> ' . esc_html((string) $posts) . '</span>';
echo '<span><i class="fas fa-comment" aria-hidden="true"></i> ' . esc_html((string) $reply) . '</span>';
echo '<span><i class="fas fa-heart" aria-hidden="true"></i> ' . esc_html((string) $follow) . '</span>';
echo '</div></div></div>';
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
private function resolve_current_plate(): ?\WP_Post {
if (is_singular('aksrt_forum_plate')) {
$p = get_queried_object();
return $p instanceof \WP_Post ? $p : null;
}
if (is_singular('aksrt_forum_topic')) {
$topic_id = (int) get_queried_object_id();
$plate_id = (int) get_post_meta($topic_id, '_aksrt_plate_id', true);
if ($plate_id <= 0) {
// Fallback: first linked board term → plate meta, or parent.
$boards = get_the_terms($topic_id, 'aksrt_forum_board');
if (!is_wp_error($boards) && !empty($boards[0])) {
$linked = (int) get_term_meta((int) $boards[0]->term_id, '_aksrt_plate_id', true);
if ($linked > 0) {
$plate_id = $linked;
}
}
}
if ($plate_id > 0) {
$p = get_post($plate_id);
if ($p instanceof \WP_Post && $p->post_type === 'aksrt_forum_plate') {
return $p;
}
}
}
return null;
}
public function form($instance): void {
echo '<p class="description">' . esc_html__('无需配置。仅在版块页或帖子页显示对应版块信息。', 'aksrt') . '</p>';
}
public function update($new_instance, $old_instance): array {
return [];
}
}
+216
View File
@@ -0,0 +1,216 @@
<?php
/**
* Forum plate list (zibll zib_bbs_widget_ui_plate_lists lite).
*
* @package AKSRT
*/
namespace AKSRT\Widgets;
defined('ABSPATH') || exit;
class WidgetForumPlateLists extends \WP_Widget {
public function __construct() {
parent::__construct(
'aksrt_forum_plate_lists',
esc_html__('论坛 - 版块列表(Plate)', 'aksrt'),
[
'description' => esc_html__('展示论坛 plate 版块列表,支持排序与卡片样式。', 'aksrt'),
'classname' => 'widget_aksrt_forum_plate_lists',
]
);
}
public function widget($args, $instance): void {
if (!post_type_exists('aksrt_forum_plate')) {
return;
}
$limit = max(1, min(30, (int) ($instance['showposts'] ?? 8)));
$orderby = sanitize_key((string) ($instance['orderby'] ?? 'menu_order'));
$style = sanitize_key((string) ($instance['style'] ?? 'default'));
$filter = sanitize_key((string) ($instance['filter'] ?? ''));
$cat_raw = (string) ($instance['cat'] ?? '');
$cat_ids = array_values(array_filter(array_map('intval', preg_split('/[\s,]+/', $cat_raw) ?: [])));
$query_args = [
'post_type' => 'aksrt_forum_plate',
'post_status' => 'publish',
'posts_per_page' => $limit,
'order' => 'ASC',
];
switch ($orderby) {
case 'date':
$query_args['orderby'] = 'date';
$query_args['order'] = 'DESC';
break;
case 'posts':
$query_args['meta_key'] = '_aksrt_posts_count';
$query_args['orderby'] = 'meta_value_num';
$query_args['order'] = 'DESC';
break;
case 'views':
$query_args['meta_key'] = '_aksrt_views';
$query_args['orderby'] = 'meta_value_num';
$query_args['order'] = 'DESC';
break;
case 'follow':
$query_args['meta_key'] = '_aksrt_follow_count';
$query_args['orderby'] = 'meta_value_num';
$query_args['order'] = 'DESC';
break;
default:
$query_args['orderby'] = 'menu_order title';
}
if ($cat_ids) {
$query_args['tax_query'] = [[
'taxonomy' => 'aksrt_plate_cat',
'field' => 'term_id',
'terms' => $cat_ids,
]];
}
if ($filter === 'is_hot') {
$query_args['meta_query'] = [[
'key' => '_aksrt_is_hot',
'value' => '1',
'compare' => '=',
]];
}
$plates = get_posts($query_args);
if (!$plates) {
return;
}
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
if ($title !== '') {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
if ($style === 'card') {
echo '<div class="aksrt-plate-cards">';
foreach ($plates as $post) {
$this->render_card($post);
}
echo '</div>';
} else {
echo '<div class="aksrt-plate-lists">';
foreach ($plates as $post) {
$this->render_row($post);
}
echo '</div>';
}
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
private function render_row(\WP_Post $post): void {
$id = (int) $post->ID;
$url = get_permalink($id);
$thumb = get_the_post_thumbnail_url($id, 'thumbnail');
$icon = (string) get_post_meta($id, '_aksrt_icon', true);
$count = (int) get_post_meta($id, '_aksrt_posts_count', true);
$desc = $post->post_excerpt !== '' ? $post->post_excerpt : wp_trim_words(wp_strip_all_tags($post->post_content), 18);
echo '<a class="aksrt-plate-item" href="' . esc_url($url) . '">';
echo '<div class="aksrt-plate-item__thumb">';
if ($thumb) {
echo '<img src="' . esc_url($thumb) . '" alt="" loading="lazy" />';
} elseif ($icon !== '') {
echo '<i class="' . esc_attr($icon) . '" aria-hidden="true"></i>';
} else {
echo '<i class="fas fa-comments" aria-hidden="true"></i>';
}
echo '</div>';
echo '<div class="aksrt-plate-item__body">';
echo '<div class="aksrt-plate-item__title">' . esc_html(get_the_title($post)) . '</div>';
if ($desc !== '') {
echo '<div class="aksrt-plate-item__desc">' . esc_html($desc) . '</div>';
}
echo '<div class="aksrt-plate-item__meta"><span>' . esc_html(sprintf(/* translators: %d topic count */ __('%d 帖', 'aksrt'), $count)) . '</span></div>';
echo '</div></a>';
}
private function render_card(\WP_Post $post): void {
$id = (int) $post->ID;
$url = get_permalink($id);
$thumb = get_the_post_thumbnail_url($id, 'medium');
$count = (int) get_post_meta($id, '_aksrt_posts_count', true);
$follow = (int) get_post_meta($id, '_aksrt_follow_count', true);
echo '<a class="aksrt-plate-card" href="' . esc_url($url) . '">';
echo '<div class="aksrt-plate-card__cover"' . ($thumb ? ' style="background-image:url(' . esc_url($thumb) . ')"' : '') . '></div>';
echo '<div class="aksrt-plate-card__title">' . esc_html(get_the_title($post)) . '</div>';
echo '<div class="aksrt-plate-card__meta">';
echo '<span>' . esc_html(sprintf(__('%d 帖', 'aksrt'), $count)) . '</span>';
echo '<span>' . esc_html(sprintf(__('%d 关注', 'aksrt'), $follow)) . '</span>';
echo '</div></a>';
}
public function form($instance): void {
?>
<p>
<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'] ?? __('版块列表', 'aksrt'))); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('数量', 'aksrt'); ?>
<input class="tiny-text" type="number" min="1" max="30" name="<?php echo esc_attr($this->get_field_name('showposts')); ?>" value="<?php echo esc_attr((string) ((int) ($instance['showposts'] ?? 8))); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('排序', 'aksrt'); ?>
<select name="<?php echo esc_attr($this->get_field_name('orderby')); ?>">
<option value="menu_order" <?php selected(($instance['orderby'] ?? 'menu_order'), 'menu_order'); ?>><?php esc_html_e('菜单顺序', 'aksrt'); ?></option>
<option value="date" <?php selected(($instance['orderby'] ?? ''), 'date'); ?>><?php esc_html_e('创建时间', 'aksrt'); ?></option>
<option value="posts" <?php selected(($instance['orderby'] ?? ''), 'posts'); ?>><?php esc_html_e('帖子数', 'aksrt'); ?></option>
<option value="views" <?php selected(($instance['orderby'] ?? ''), 'views'); ?>><?php esc_html_e('浏览量', 'aksrt'); ?></option>
<option value="follow" <?php selected(($instance['orderby'] ?? ''), 'follow'); ?>><?php esc_html_e('关注数', 'aksrt'); ?></option>
</select>
</label>
</p>
<p>
<label><?php esc_html_e('样式', 'aksrt'); ?>
<select name="<?php echo esc_attr($this->get_field_name('style')); ?>">
<option value="default" <?php selected(($instance['style'] ?? 'default'), 'default'); ?>><?php esc_html_e('列表', 'aksrt'); ?></option>
<option value="card" <?php selected(($instance['style'] ?? ''), 'card'); ?>><?php esc_html_e('卡片', 'aksrt'); ?></option>
</select>
</label>
</p>
<p>
<label><?php esc_html_e('筛选', 'aksrt'); ?>
<select name="<?php echo esc_attr($this->get_field_name('filter')); ?>">
<option value="" <?php selected(($instance['filter'] ?? ''), ''); ?>><?php esc_html_e('全部', 'aksrt'); ?></option>
<option value="is_hot" <?php selected(($instance['filter'] ?? ''), 'is_hot'); ?>><?php esc_html_e('仅热门', 'aksrt'); ?></option>
</select>
</label>
</p>
<p>
<label><?php esc_html_e('版块分类 term ID(逗号)', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('cat')); ?>" value="<?php echo esc_attr((string) ($instance['cat'] ?? '')); ?>">
</label>
</p>
<?php
}
public function update($new_instance, $old_instance): array {
$orderby = sanitize_key((string) ($new_instance['orderby'] ?? 'menu_order'));
if (!in_array($orderby, ['menu_order', 'date', 'posts', 'views', 'follow'], true)) {
$orderby = 'menu_order';
}
return [
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
'showposts' => max(1, min(30, (int) ($new_instance['showposts'] ?? 8))),
'orderby' => $orderby,
'style' => ($new_instance['style'] ?? '') === 'card' ? 'card' : 'default',
'filter' => ($new_instance['filter'] ?? '') === 'is_hot' ? 'is_hot' : '',
'cat' => sanitize_text_field((string) ($new_instance['cat'] ?? '')),
];
}
}
@@ -0,0 +1,131 @@
<?php
/**
* Current plate moderators (zibll zib_bbs_widget_ui_plate_moderator).
*
* @package AKSRT
*/
namespace AKSRT\Widgets;
defined('ABSPATH') || exit;
class WidgetForumPlateModerator extends \WP_Widget {
public function __construct() {
parent::__construct(
'aksrt_forum_plate_moderator',
esc_html__('论坛 - 版主列表', 'aksrt'),
[
'description' => esc_html__('显示当前版块的版主与超级版主。', 'aksrt'),
'classname' => 'widget_aksrt_forum_plate_moderator',
]
);
}
public function widget($args, $instance): void {
$plate = $this->resolve_current_plate();
if (!$plate) {
return;
}
$id = (int) $plate->ID;
$author_id = (int) $plate->post_author;
$mods = get_post_meta($id, '_aksrt_moderators', true);
if (!is_array($mods)) {
$mods = [];
}
$mods = array_values(array_unique(array_filter(array_map('intval', $mods))));
$people = [];
if ($author_id > 0) {
$people[] = ['id' => $author_id, 'role' => 'author'];
}
foreach ($mods as $mid) {
if ($mid === $author_id) {
continue;
}
$people[] = ['id' => $mid, 'role' => 'moderator'];
}
if (!$people) {
return;
}
$title_text = (string) ($instance['title_text'] ?? __('版主', 'aksrt'));
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div class="aksrt-plate-mods">';
if ($title_text !== '') {
echo '<div class="aksrt-plate-mods__head"><div class="title-theme">' . esc_html($title_text) . '</div></div>';
}
echo '<div class="aksrt-plate-mods__list">';
foreach ($people as $row) {
$uid = (int) $row['id'];
$user = get_userdata($uid);
if (!$user) {
continue;
}
$avatar = function_exists('aksrt_get_avatar_url')
? aksrt_get_avatar_url($uid, ['size' => 64])
: get_avatar_url($uid, ['size' => 64]);
$home = function_exists('aksrt_get_user_home_url')
? aksrt_get_user_home_url($uid)
: get_author_posts_url($uid);
$role_label = $row['role'] === 'author'
? __('超级版主', 'aksrt')
: __('版主', 'aksrt');
echo '<a class="aksrt-plate-mods__item" href="' . esc_url($home) . '">';
echo '<img src="' . esc_url($avatar) . '" alt="" class="aksrt-plate-mods__avatar" loading="lazy" width="40" height="40" />';
echo '<span class="aksrt-plate-mods__name">' . esc_html($user->display_name) . '</span>';
echo '<span class="aksrt-plate-mods__role">' . esc_html($role_label) . '</span>';
echo '</a>';
}
echo '</div></div>';
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
private function resolve_current_plate(): ?\WP_Post {
if (is_singular('aksrt_forum_plate')) {
$p = get_queried_object();
return $p instanceof \WP_Post ? $p : null;
}
if (is_singular('aksrt_forum_topic')) {
$topic_id = (int) get_queried_object_id();
$plate_id = (int) get_post_meta($topic_id, '_aksrt_plate_id', true);
if ($plate_id <= 0) {
$boards = get_the_terms($topic_id, 'aksrt_forum_board');
if (!is_wp_error($boards) && !empty($boards[0])) {
$linked = (int) get_term_meta((int) $boards[0]->term_id, '_aksrt_plate_id', true);
if ($linked > 0) {
$plate_id = $linked;
}
}
}
if ($plate_id > 0) {
$p = get_post($plate_id);
if ($p instanceof \WP_Post && $p->post_type === 'aksrt_forum_plate') {
return $p;
}
}
}
return null;
}
public function form($instance): void {
?>
<p>
<label><?php esc_html_e('标题文案', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('title_text')); ?>" value="<?php echo esc_attr((string) ($instance['title_text'] ?? __('版主', 'aksrt'))); ?>">
</label>
</p>
<p class="description"><?php esc_html_e('仅在版块/帖子页显示。', 'aksrt'); ?></p>
<?php
}
public function update($new_instance, $old_instance): array {
return [
'title_text' => sanitize_text_field((string) ($new_instance['title_text'] ?? '')),
];
}
}
+113
View File
@@ -0,0 +1,113 @@
<?php
/**
* Super embed / iframe widget (zibll zib_widget_ui_iframe).
*
* @package AKSRT
*/
namespace AKSRT\Widgets;
defined('ABSPATH') || exit;
class WidgetIframe extends \WP_Widget {
public function __construct() {
parent::__construct(
'aksrt_iframe',
esc_html__('AKSRT 超级嵌入', 'aksrt'),
[
'description' => esc_html__('嵌入在线视频/音乐播放器或任意 iframe 内容。', 'aksrt'),
'classname' => 'widget_aksrt_iframe',
]
);
}
public function widget($args, $instance): void {
$raw = trim((string) ($instance['url'] ?? ''));
if ($raw === '') {
return;
}
$aspect = max(20, min(300, (int) ($instance['aspect'] ?? 55)));
$allow_fs = !empty($instance['allowfullscreen']);
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
if ($title !== '') {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
$iframe_html = '';
if (stripos($raw, '<iframe') !== false && stripos($raw, '</iframe>') !== false) {
$iframe_html = wp_kses(
$raw,
[
'iframe' => [
'src' => true,
'width' => true,
'height' => true,
'frameborder' => true,
'allow' => true,
'allowfullscreen' => true,
'loading' => true,
'class' => true,
'style' => true,
'referrerpolicy' => true,
'title' => true,
'sandbox' => true,
'data-src' => true,
],
]
);
} else {
$url = esc_url($raw);
if ($url === '') {
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
return;
}
$fs = $allow_fs ? ' allowfullscreen="allowfullscreen"' : '';
$iframe_html = '<iframe class="aksrt-iframe-el" src="' . $url . '" loading="lazy" frameborder="0"' . $fs . '></iframe>';
}
echo '<div class="aksrt-iframe-embed">';
echo '<div class="aksrt-iframe-ratio" style="padding-bottom:' . esc_attr((string) $aspect) . '%">';
echo $iframe_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '</div></div>';
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
public function form($instance): void {
$title = (string) ($instance['title'] ?? '');
$url = (string) ($instance['url'] ?? '');
$aspect = (int) ($instance['aspect'] ?? 55);
?>
<p>
<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($title); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('嵌入地址或 iframe 代码', 'aksrt'); ?>
<textarea class="widefat" rows="4" name="<?php echo esc_attr($this->get_field_name('url')); ?>"><?php echo esc_textarea($url); ?></textarea>
</label>
</p>
<p>
<label><?php esc_html_e('高度占比 %', 'aksrt'); ?>
<input class="tiny-text" type="number" min="20" max="300" step="5" name="<?php echo esc_attr($this->get_field_name('aspect')); ?>" value="<?php echo esc_attr((string) $aspect); ?>">
</label>
</p>
<p>
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('allowfullscreen')); ?>" value="1" <?php checked(!empty($instance['allowfullscreen'])); ?>> <?php esc_html_e('允许全屏', 'aksrt'); ?></label>
</p>
<?php
}
public function update($new_instance, $old_instance): array {
return [
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
'url' => trim((string) ($new_instance['url'] ?? '')),
'aspect' => max(20, min(300, (int) ($new_instance['aspect'] ?? 55))),
'allowfullscreen' => !empty($new_instance['allowfullscreen']) ? 1 : 0,
];
}
}
+348
View File
@@ -0,0 +1,348 @@
<?php
/**
* Horizontal partners / friends scroll (zibll zib_widget_ui_partners_scroll).
*
* @package AKSRT
*/
namespace AKSRT\Widgets;
defined('ABSPATH') || exit;
class WidgetPartnersScroll extends \WP_Widget {
public function __construct() {
parent::__construct(
'aksrt_partners_scroll',
esc_html__('AKSRT 合作伙伴滚动', 'aksrt'),
[
'description' => esc_html__('横向无缝滚动的合作伙伴/友情链接条。', 'aksrt'),
'classname' => 'widget_aksrt_partners_scroll',
]
);
}
public function widget($args, $instance): void {
$partners = $this->resolve_partners($instance);
if (!$partners) {
return;
}
$rows = max(1, min(4, (int) ($instance['rows'] ?? 1)));
$m_rows = max(1, min(4, (int) ($instance['m_rows'] ?? 1)));
$style_type = sanitize_key((string) ($instance['style_type'] ?? 'card'));
if (!in_array($style_type, ['card', 'logo', 'chip'], true)) {
$style_type = 'card';
}
$card_w = max(60, min(400, (int) ($instance['card_width'] ?? 220)));
$card_h = max(40, min(200, (int) ($instance['card_height'] ?? 90)));
$speed = max(1, min(20, (int) ($instance['speed'] ?? 3)));
$direction = sanitize_key((string) ($instance['direction'] ?? 'alternate'));
$pause = !empty($instance['pause_hover']) || !isset($instance['pause_hover']);
$card_bg = !empty($instance['card_bg']) || !isset($instance['card_bg']);
$end_fade = !empty($instance['end_fade']) || !isset($instance['end_fade']);
$go_link = !empty($instance['go_link']);
$show_bg = !empty($instance['show_widget_bg']) || !isset($instance['show_widget_bg']);
$radius = max(0, min(40, (int) ($instance['card_radius'] ?? 10)));
$use_rows = wp_is_mobile() ? $m_rows : $rows;
$rows_data = array_fill(0, $use_rows, []);
foreach (array_values($partners) as $i => $p) {
$rows_data[$i % $use_rows][] = $p;
}
$rows_data = array_values(array_filter($rows_data, static fn($r) => !empty($r)));
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
if ($title !== '') {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
$wrap_class = 'aksrt-partners-scroll aksrt-ps-style-' . $style_type;
if ($pause) {
$wrap_class .= ' aksrt-ps-pause-hover';
}
if ($card_bg) {
$wrap_class .= ' aksrt-ps-card-bg';
}
if ($end_fade) {
$wrap_class .= ' aksrt-ps-end-fade';
}
$style = sprintf(
'--aksrt-ps-w:%dpx;--aksrt-ps-h:%dpx;--aksrt-ps-radius:%dpx;--aksrt-ps-speed:%d;',
$card_w,
$card_h,
$radius,
$speed
);
$outer = $show_bg ? 'aksrt-partners-scroll-wrap' : 'aksrt-partners-scroll-wrap is-bare';
echo '<div class="' . esc_attr($outer) . '">';
echo '<div class="' . esc_attr($wrap_class) . '" style="' . esc_attr($style) . '">';
foreach ($rows_data as $row_index => $row_partners) {
switch ($direction) {
case 'left':
$dir = 'left';
break;
case 'right':
$dir = 'right';
break;
default:
$dir = ($row_index % 2 === 0) ? 'left' : 'right';
}
echo '<div class="aksrt-ps-row aksrt-ps-dir-' . esc_attr($dir) . '">';
echo '<div class="aksrt-ps-track">';
// Duplicate group for seamless CSS marquee.
for ($dup = 0; $dup < 2; $dup++) {
echo '<div class="aksrt-ps-group">';
foreach ($row_partners as $partner) {
echo $this->render_card($partner, $style_type, $go_link); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
echo '</div>';
}
echo '</div></div>';
}
echo '</div></div>';
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* @param array<string,mixed> $partner
*/
private function render_card(array $partner, string $style_type, bool $go_link): string {
$title = trim((string) ($partner['title'] ?? ''));
$desc = trim((string) ($partner['desc'] ?? ''));
$image = (string) ($partner['image'] ?? '');
$url = (string) ($partner['url'] ?? '');
$target = (string) ($partner['target'] ?? '');
if ($image === '' && $title === '') {
return '';
}
$alt = esc_attr($title !== '' ? $title : __('合作伙伴', 'aksrt'));
$img_tag = $image !== ''
? '<img class="aksrt-ps-img" src="' . esc_url($image) . '" alt="' . $alt . '" loading="lazy" />'
: '';
$inner = '';
if ($style_type === 'logo') {
if ($img_tag === '') {
return '';
}
$inner = $img_tag;
} elseif ($style_type === 'chip') {
if ($img_tag !== '') {
$inner .= '<span class="aksrt-ps-chip-icon">' . $img_tag . '</span>';
}
$inner .= '<span class="aksrt-ps-chip-title">' . esc_html($title) . '</span>';
} else {
if ($img_tag !== '') {
$inner .= '<div class="aksrt-ps-card-icon">' . $img_tag . '</div>';
}
$inner .= '<div class="aksrt-ps-card-text">';
if ($title !== '') {
$inner .= '<div class="aksrt-ps-card-title">' . esc_html($title) . '</div>';
}
if ($desc !== '') {
$inner .= '<div class="aksrt-ps-card-desc">' . esc_html($desc) . '</div>';
}
$inner .= '</div>';
}
if ($url !== '') {
if ($go_link && function_exists('aksrt_go_link')) {
$url = aksrt_go_link($url, true);
}
$tgt = $target !== '' ? ' target="' . esc_attr($target) . '" rel="noopener noreferrer"' : '';
return '<a class="aksrt-ps-item" href="' . esc_url($url) . '"' . $tgt . '>' . $inner . '</a>';
}
return '<div class="aksrt-ps-item">' . $inner . '</div>';
}
/**
* @param array<string,mixed> $instance
* @return array<int, array{title:string,desc:string,image:string,url:string,target:string}>
*/
private function resolve_partners(array $instance): array {
$source = sanitize_key((string) ($instance['link_type'] ?? 'manual'));
if ($source === 'link') {
$args = [
'orderby' => sanitize_key((string) ($instance['link_orderby'] ?? 'name')) ?: 'name',
'order' => strtoupper((string) ($instance['link_order'] ?? 'ASC')) === 'DESC' ? 'DESC' : 'ASC',
'limit' => max(1, min(100, (int) ($instance['link_limit'] ?? 50))),
];
$cats = $instance['link_cats'] ?? '';
if (is_array($cats)) {
$cats = implode(',', array_map('intval', $cats));
}
$cats = trim((string) $cats);
if ($cats !== '') {
$args['category'] = $cats;
}
$bookmarks = get_bookmarks($args);
$out = [];
foreach ($bookmarks as $b) {
$out[] = [
'title' => (string) $b->link_name,
'desc' => (string) $b->link_description,
'image' => (string) $b->link_image,
'url' => (string) $b->link_url,
'target' => (string) $b->link_target,
];
}
return $out;
}
return $this->normalize_manual($instance['partners'] ?? []);
}
/**
* @param mixed $raw
* @return array<int, array{title:string,desc:string,image:string,url:string,target:string}>
*/
private function normalize_manual($raw): array {
if (is_string($raw)) {
$decoded = json_decode($raw, true);
$raw = is_array($decoded) ? $decoded : [];
}
if (!is_array($raw)) {
return [];
}
$out = [];
foreach ($raw as $row) {
if (!is_array($row)) {
continue;
}
$title = sanitize_text_field((string) ($row['title'] ?? ''));
$image = esc_url_raw(trim((string) ($row['image'] ?? '')));
$url = esc_url_raw(trim((string) ($row['url'] ?? $row['link'] ?? '')));
if ($title === '' && $image === '') {
continue;
}
$out[] = [
'title' => $title,
'desc' => sanitize_text_field((string) ($row['desc'] ?? '')),
'image' => $image,
'url' => $url,
'target' => sanitize_text_field((string) ($row['target'] ?? '_blank')),
];
if (count($out) >= 40) {
break;
}
}
return $out;
}
public function form($instance): void {
$title = (string) ($instance['title'] ?? '');
$link_type = (string) ($instance['link_type'] ?? 'manual');
$partners_json = is_string($instance['partners'] ?? null)
? (string) $instance['partners']
: wp_json_encode($this->normalize_manual($instance['partners'] ?? []), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
?>
<p>
<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($title); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('数据来源', 'aksrt'); ?>
<select class="widefat" name="<?php echo esc_attr($this->get_field_name('link_type')); ?>">
<option value="manual" <?php selected($link_type, 'manual'); ?>><?php esc_html_e('手动 JSON', 'aksrt'); ?></option>
<option value="link" <?php selected($link_type, 'link'); ?>><?php esc_html_e('WordPress 链接(书签)', 'aksrt'); ?></option>
</select>
</label>
</p>
<p>
<label><?php esc_html_e('合作伙伴 JSON', 'aksrt'); ?>
<textarea class="widefat" rows="6" name="<?php echo esc_attr($this->get_field_name('partners')); ?>" placeholder='[{"title":"Name","image":"https://...","url":"https://...","desc":""}]'><?php echo esc_textarea((string) $partners_json); ?></textarea>
</label>
<small><?php esc_html_e('字段: title, image, url, desc, target', 'aksrt'); ?></small>
</p>
<p>
<label><?php esc_html_e('链接分类 ID(逗号分隔,书签模式)', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('link_cats')); ?>" value="<?php echo esc_attr((string) ($instance['link_cats'] ?? '')); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('书签数量上限', 'aksrt'); ?>
<input class="tiny-text" type="number" min="1" max="100" name="<?php echo esc_attr($this->get_field_name('link_limit')); ?>" value="<?php echo esc_attr((string) ((int) ($instance['link_limit'] ?? 50))); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('样式', 'aksrt'); ?>
<select name="<?php echo esc_attr($this->get_field_name('style_type')); ?>">
<option value="card" <?php selected(($instance['style_type'] ?? 'card'), 'card'); ?>><?php esc_html_e('图文卡片', 'aksrt'); ?></option>
<option value="logo" <?php selected(($instance['style_type'] ?? ''), 'logo'); ?>><?php esc_html_e('纯 Logo', 'aksrt'); ?></option>
<option value="chip" <?php selected(($instance['style_type'] ?? ''), 'chip'); ?>><?php esc_html_e('胶囊', 'aksrt'); ?></option>
</select>
</label>
</p>
<p>
<label><?php esc_html_e('PC 行数', 'aksrt'); ?>
<input class="tiny-text" type="number" min="1" max="4" name="<?php echo esc_attr($this->get_field_name('rows')); ?>" value="<?php echo esc_attr((string) ((int) ($instance['rows'] ?? 1))); ?>">
</label>
<label><?php esc_html_e('移动行数', 'aksrt'); ?>
<input class="tiny-text" type="number" min="1" max="4" name="<?php echo esc_attr($this->get_field_name('m_rows')); ?>" value="<?php echo esc_attr((string) ((int) ($instance['m_rows'] ?? 1))); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('卡片宽', 'aksrt'); ?>
<input class="tiny-text" type="number" min="60" max="400" name="<?php echo esc_attr($this->get_field_name('card_width')); ?>" value="<?php echo esc_attr((string) ((int) ($instance['card_width'] ?? 220))); ?>">
</label>
<label><?php esc_html_e('高', 'aksrt'); ?>
<input class="tiny-text" type="number" min="40" max="200" name="<?php echo esc_attr($this->get_field_name('card_height')); ?>" value="<?php echo esc_attr((string) ((int) ($instance['card_height'] ?? 90))); ?>">
</label>
<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('speed')); ?>" value="<?php echo esc_attr((string) ((int) ($instance['speed'] ?? 3))); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('方向', 'aksrt'); ?>
<select name="<?php echo esc_attr($this->get_field_name('direction')); ?>">
<option value="alternate" <?php selected(($instance['direction'] ?? 'alternate'), 'alternate'); ?>><?php esc_html_e('交替', 'aksrt'); ?></option>
<option value="left" <?php selected(($instance['direction'] ?? ''), 'left'); ?>><?php esc_html_e('向左', 'aksrt'); ?></option>
<option value="right" <?php selected(($instance['direction'] ?? ''), 'right'); ?>><?php esc_html_e('向右', 'aksrt'); ?></option>
</select>
</label>
</p>
<p>
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('pause_hover')); ?>" value="1" <?php checked(!empty($instance['pause_hover']) || !isset($instance['pause_hover'])); ?>> <?php esc_html_e('悬停暂停', 'aksrt'); ?></label><br>
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('card_bg')); ?>" value="1" <?php checked(!empty($instance['card_bg']) || !isset($instance['card_bg'])); ?>> <?php esc_html_e('卡片背景', 'aksrt'); ?></label><br>
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('end_fade')); ?>" value="1" <?php checked(!empty($instance['end_fade']) || !isset($instance['end_fade'])); ?>> <?php esc_html_e('两端渐隐', 'aksrt'); ?></label><br>
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('go_link')); ?>" value="1" <?php checked(!empty($instance['go_link'])); ?>> <?php esc_html_e('外链跳转页', 'aksrt'); ?></label><br>
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('show_widget_bg')); ?>" value="1" <?php checked(!empty($instance['show_widget_bg']) || !isset($instance['show_widget_bg'])); ?>> <?php esc_html_e('显示背景盒子', 'aksrt'); ?></label>
</p>
<?php
}
public function update($new_instance, $old_instance): array {
return [
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
'link_type' => in_array(($new_instance['link_type'] ?? ''), ['manual', 'link'], true) ? $new_instance['link_type'] : 'manual',
'partners' => $this->normalize_manual($new_instance['partners'] ?? []),
'link_cats' => sanitize_text_field((string) ($new_instance['link_cats'] ?? '')),
'link_limit' => max(1, min(100, (int) ($new_instance['link_limit'] ?? 50))),
'link_orderby' => sanitize_key((string) ($new_instance['link_orderby'] ?? 'name')),
'link_order' => strtoupper((string) ($new_instance['link_order'] ?? 'ASC')) === 'DESC' ? 'DESC' : 'ASC',
'style_type' => in_array(($new_instance['style_type'] ?? ''), ['card', 'logo', 'chip'], true) ? $new_instance['style_type'] : 'card',
'rows' => max(1, min(4, (int) ($new_instance['rows'] ?? 1))),
'm_rows' => max(1, min(4, (int) ($new_instance['m_rows'] ?? 1))),
'card_width' => max(60, min(400, (int) ($new_instance['card_width'] ?? 220))),
'card_height' => max(40, min(200, (int) ($new_instance['card_height'] ?? 90))),
'card_radius' => max(0, min(40, (int) ($new_instance['card_radius'] ?? 10))),
'speed' => max(1, min(20, (int) ($new_instance['speed'] ?? 3))),
'direction' => in_array(($new_instance['direction'] ?? ''), ['alternate', 'left', 'right'], true) ? $new_instance['direction'] : 'alternate',
'pause_hover' => !empty($new_instance['pause_hover']) ? 1 : 0,
'card_bg' => !empty($new_instance['card_bg']) ? 1 : 0,
'end_fade' => !empty($new_instance['end_fade']) ? 1 : 0,
'go_link' => !empty($new_instance['go_link']) ? 1 : 0,
'show_widget_bg' => !empty($new_instance['show_widget_bg']) ? 1 : 0,
];
}
}
+138
View File
@@ -0,0 +1,138 @@
<?php
/**
* Text title / section heading (zibll zib_widget_ui_text_title lite).
*
* @package AKSRT
*/
namespace AKSRT\Widgets;
defined('ABSPATH') || exit;
class WidgetTextTitle extends \WP_Widget {
public function __construct() {
parent::__construct(
'aksrt_text_title',
esc_html__('AKSRT 文字标题', 'aksrt'),
[
'description' => esc_html__('自定义标题 + 副标题区块,用于模块分隔。', 'aksrt'),
'classname' => 'widget_aksrt_text_title',
]
);
}
public function widget($args, $instance): void {
$title = trim((string) ($instance['title'] ?? ''));
$desc = trim((string) ($instance['desc'] ?? ''));
if ($title === '' && $desc === '') {
return;
}
$align = sanitize_key((string) ($instance['align'] ?? 'center'));
if (!in_array($align, ['left', 'center', 'right'], true)) {
$align = 'center';
}
$icon_left = sanitize_text_field((string) ($instance['title_icon_left'] ?? ''));
$icon_right = sanitize_text_field((string) ($instance['title_icon_right'] ?? ''));
$title_size = max(12, min(48, (int) ($instance['title_size'] ?? 22)));
$desc_size = max(11, min(24, (int) ($instance['desc_size'] ?? 14)));
$title_color = sanitize_hex_color((string) ($instance['title_color'] ?? '')) ?: '';
$desc_color = sanitize_hex_color((string) ($instance['desc_color'] ?? '')) ?: '';
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$style = sprintf(
'--aksrt-tt-title-size:%dpx;--aksrt-tt-desc-size:%dpx;%s%s',
$title_size,
$desc_size,
$title_color !== '' ? '--aksrt-tt-title-color:' . $title_color . ';' : '',
$desc_color !== '' ? '--aksrt-tt-desc-color:' . $desc_color . ';' : ''
);
echo '<div class="aksrt-text-title align-' . esc_attr($align) . '" style="' . esc_attr($style) . '">';
if ($title !== '') {
echo '<div class="aksrt-text-title__main">';
if ($icon_left !== '') {
echo '<span class="aksrt-text-title__icon"><i class="' . esc_attr($icon_left) . '" aria-hidden="true"></i></span>';
}
echo '<span class="aksrt-text-title__text">' . esc_html($title) . '</span>';
if ($icon_right !== '') {
echo '<span class="aksrt-text-title__icon"><i class="' . esc_attr($icon_right) . '" aria-hidden="true"></i></span>';
}
echo '</div>';
}
if ($desc !== '') {
echo '<div class="aksrt-text-title__desc">' . esc_html($desc) . '</div>';
}
echo '</div>';
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
public function form($instance): void {
?>
<p>
<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'] ?? '')); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('副标题 / 描述', 'aksrt'); ?>
<textarea class="widefat" rows="2" name="<?php echo esc_attr($this->get_field_name('desc')); ?>"><?php echo esc_textarea((string) ($instance['desc'] ?? '')); ?></textarea>
</label>
</p>
<p>
<label><?php esc_html_e('对齐', 'aksrt'); ?>
<select name="<?php echo esc_attr($this->get_field_name('align')); ?>">
<option value="center" <?php selected(($instance['align'] ?? 'center'), 'center'); ?>><?php esc_html_e('居中', 'aksrt'); ?></option>
<option value="left" <?php selected(($instance['align'] ?? ''), 'left'); ?>><?php esc_html_e('左', 'aksrt'); ?></option>
<option value="right" <?php selected(($instance['align'] ?? ''), 'right'); ?>><?php esc_html_e('右', 'aksrt'); ?></option>
</select>
</label>
</p>
<p>
<label><?php esc_html_e('左图标 class', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('title_icon_left')); ?>" value="<?php echo esc_attr((string) ($instance['title_icon_left'] ?? '')); ?>" placeholder="fas fa-star">
</label>
</p>
<p>
<label><?php esc_html_e('右图标 class', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('title_icon_right')); ?>" value="<?php echo esc_attr((string) ($instance['title_icon_right'] ?? '')); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('标题字号', 'aksrt'); ?>
<input class="tiny-text" type="number" min="12" max="48" name="<?php echo esc_attr($this->get_field_name('title_size')); ?>" value="<?php echo esc_attr((string) ((int) ($instance['title_size'] ?? 22))); ?>">
</label>
<label><?php esc_html_e('描述字号', 'aksrt'); ?>
<input class="tiny-text" type="number" min="11" max="24" name="<?php echo esc_attr($this->get_field_name('desc_size')); ?>" value="<?php echo esc_attr((string) ((int) ($instance['desc_size'] ?? 14))); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('标题色 #hex', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('title_color')); ?>" value="<?php echo esc_attr((string) ($instance['title_color'] ?? '')); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('描述色 #hex', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('desc_color')); ?>" value="<?php echo esc_attr((string) ($instance['desc_color'] ?? '')); ?>">
</label>
</p>
<?php
}
public function update($new_instance, $old_instance): array {
return [
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
'desc' => sanitize_text_field((string) ($new_instance['desc'] ?? '')),
'align' => in_array(($new_instance['align'] ?? ''), ['left', 'center', 'right'], true) ? $new_instance['align'] : 'center',
'title_icon_left' => sanitize_text_field((string) ($new_instance['title_icon_left'] ?? '')),
'title_icon_right' => sanitize_text_field((string) ($new_instance['title_icon_right'] ?? '')),
'title_size' => max(12, min(48, (int) ($new_instance['title_size'] ?? 22))),
'desc_size' => max(11, min(24, (int) ($new_instance['desc_size'] ?? 14))),
'title_color' => sanitize_hex_color((string) ($new_instance['title_color'] ?? '')) ?: '',
'desc_color' => sanitize_hex_color((string) ($new_instance['desc_color'] ?? '')) ?: '',
];
}
}
+199
View File
@@ -0,0 +1,199 @@
<?php
/**
* Registered user list (zibll widget_ui_user_lists).
*
* @package AKSRT
*/
namespace AKSRT\Widgets;
defined('ABSPATH') || exit;
class WidgetUserLists extends \WP_Widget {
public function __construct() {
parent::__construct(
'aksrt_user_lists',
esc_html__('AKSRT 用户列表', 'aksrt'),
[
'description' => esc_html__('展示站点用户头像网格,支持排序与筛选。', 'aksrt'),
'classname' => 'widget_aksrt_user_lists',
]
);
}
public function widget($args, $instance): void {
$number = max(1, min(50, (int) ($instance['number'] ?? 8)));
$orderby = sanitize_key((string) ($instance['orderby'] ?? 'user_registered'));
$order = strtoupper((string) ($instance['order'] ?? 'DESC')) === 'ASC' ? 'ASC' : 'DESC';
$include = $this->parse_ids((string) ($instance['include'] ?? ''));
$exclude = $this->parse_ids((string) ($instance['exclude'] ?? ''));
$hide_box = !empty($instance['hide_box']);
$query = [
'number' => $number,
'order' => $order,
'fields' => ['ID', 'display_name', 'user_nicename'],
];
if ($include) {
$query['include'] = $include;
$query['orderby'] = 'include';
} else {
switch ($orderby) {
case 'display_name':
$query['orderby'] = 'display_name';
break;
case 'post_count':
$query['orderby'] = 'post_count';
break;
case 'last_login':
$query['meta_key'] = 'aksrt_last_login';
$query['orderby'] = 'meta_value';
break;
case 'followed-user-count':
// Fallback: registered date; fan ranking needs denormalized meta when available.
$query['meta_key'] = 'aksrt_follower_count';
$query['orderby'] = 'meta_value_num';
break;
default:
$query['orderby'] = 'user_registered';
}
}
if ($exclude) {
$query['exclude'] = $exclude;
}
$users = get_users($query);
if (!$users) {
return;
}
// If follower meta empty for all, re-query by registered when fan order requested.
if ($orderby === 'followed-user-count' && !$include) {
$has_meta = false;
foreach ($users as $u) {
if ((int) get_user_meta($u->ID, 'aksrt_follower_count', true) > 0) {
$has_meta = true;
break;
}
}
if (!$has_meta && class_exists('\AKSRT\User\Follow')) {
// Rank by live follower counts (small N only).
usort($users, static function ($a, $b) {
$fa = \AKSRT\User\Follow::get_instance()->get_follower_count((int) $a->ID);
$fb = \AKSRT\User\Follow::get_instance()->get_follower_count((int) $b->ID);
return $fb <=> $fa;
});
}
}
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
if ($title !== '') {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
$wrap = $hide_box ? 'aksrt-user-lists is-bare' : 'aksrt-user-lists';
echo '<div class="' . esc_attr($wrap) . '">';
foreach ($users as $user) {
$uid = (int) $user->ID;
$avatar = function_exists('aksrt_get_avatar_url')
? aksrt_get_avatar_url($uid, ['size' => 96])
: get_avatar_url($uid, ['size' => 96]);
$home = function_exists('aksrt_get_user_home_url')
? aksrt_get_user_home_url($uid)
: get_author_posts_url($uid);
$name = $user->display_name ?: $user->user_nicename;
echo '<a class="aksrt-user-lists__item" href="' . esc_url($home) . '" title="' . esc_attr($name) . '">';
echo '<img class="aksrt-user-lists__avatar" src="' . esc_url($avatar) . '" alt="' . esc_attr($name) . '" loading="lazy" width="48" height="48" />';
echo '<span class="aksrt-user-lists__name">' . esc_html($name) . '</span>';
echo '</a>';
}
echo '</div>';
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* @return array<int, int>
*/
private function parse_ids(string $raw): array {
if ($raw === '') {
return [];
}
$parts = preg_split('/[\s,]+/', $raw) ?: [];
$ids = [];
foreach ($parts as $p) {
$id = (int) $p;
if ($id > 0) {
$ids[] = $id;
}
}
return array_values(array_unique($ids));
}
public function form($instance): void {
?>
<p>
<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'] ?? '')); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('显示数量', 'aksrt'); ?>
<input class="tiny-text" type="number" min="1" max="50" name="<?php echo esc_attr($this->get_field_name('number')); ?>" value="<?php echo esc_attr((string) ((int) ($instance['number'] ?? 8))); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('包含用户 ID(逗号)', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('include')); ?>" value="<?php echo esc_attr((string) ($instance['include'] ?? '')); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('排除用户 ID(逗号)', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('exclude')); ?>" value="<?php echo esc_attr((string) ($instance['exclude'] ?? '')); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('排序', 'aksrt'); ?>
<select class="widefat" name="<?php echo esc_attr($this->get_field_name('orderby')); ?>">
<option value="user_registered" <?php selected(($instance['orderby'] ?? 'user_registered'), 'user_registered'); ?>><?php esc_html_e('注册时间', 'aksrt'); ?></option>
<option value="display_name" <?php selected(($instance['orderby'] ?? ''), 'display_name'); ?>><?php esc_html_e('昵称', 'aksrt'); ?></option>
<option value="post_count" <?php selected(($instance['orderby'] ?? ''), 'post_count'); ?>><?php esc_html_e('文章数', 'aksrt'); ?></option>
<option value="last_login" <?php selected(($instance['orderby'] ?? ''), 'last_login'); ?>><?php esc_html_e('最后登录', 'aksrt'); ?></option>
<option value="followed-user-count" <?php selected(($instance['orderby'] ?? ''), 'followed-user-count'); ?>><?php esc_html_e('粉丝数', 'aksrt'); ?></option>
</select>
</label>
</p>
<p>
<label><?php esc_html_e('顺序', 'aksrt'); ?>
<select name="<?php echo esc_attr($this->get_field_name('order')); ?>">
<option value="DESC" <?php selected(($instance['order'] ?? 'DESC'), 'DESC'); ?>><?php esc_html_e('降序', 'aksrt'); ?></option>
<option value="ASC" <?php selected(($instance['order'] ?? ''), 'ASC'); ?>><?php esc_html_e('升序', 'aksrt'); ?></option>
</select>
</label>
</p>
<p>
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('hide_box')); ?>" value="1" <?php checked(!empty($instance['hide_box'])); ?>> <?php esc_html_e('不显示背景盒子', 'aksrt'); ?></label>
</p>
<?php
}
public function update($new_instance, $old_instance): array {
$orderby = sanitize_key((string) ($new_instance['orderby'] ?? 'user_registered'));
$allowed = ['user_registered', 'display_name', 'post_count', 'last_login', 'followed-user-count'];
if (!in_array($orderby, $allowed, true)) {
$orderby = 'user_registered';
}
return [
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
'number' => max(1, min(50, (int) ($new_instance['number'] ?? 8))),
'include' => sanitize_text_field((string) ($new_instance['include'] ?? '')),
'exclude' => sanitize_text_field((string) ($new_instance['exclude'] ?? '')),
'orderby' => $orderby,
'order' => strtoupper((string) ($new_instance['order'] ?? 'DESC')) === 'ASC' ? 'ASC' : 'DESC',
'hide_box'=> !empty($new_instance['hide_box']) ? 1 : 0,
];
}
}