From d975ff4ce648d99514866ecc90ba857f2d08a359 Mon Sep 17 00:00:00 2001
From: Claude
Date: Thu, 30 Jul 2026 20:42:46 +0800
Subject: [PATCH] feat(phase2): shared common-form layer + CommonWidgetFields
trait for all widgets
- Extract Zibll CSF_Widget::params() common-field parity into
inc/modules/widget-common.php (required by widgets-register.php to keep
both files <1000 lines): subtitle, title_style(simple/big), title_highlight
+color, title_link x3, title_link_style colors, sidebar_affix, show_type
(all/only_pc/only_sm), show_id_type(show/hide), show_ids, animation_in,
animation_repeat, layout_pin_pc/m, layout_bg(white/dark). Adds
normalize_link/normalize_bg helpers, affix_attrs, common_wrapper_attr,
render_title, animation_class. Back-compat normalizes legacy tokens
(in_affix->sidebar_affix, pc/only_pc, include/exclude->show/hide).
- Add src/AKSRT/Widgets/Traits/CommonWidgetFields.php trait
(render_common_form_section + update_common_fields).
- Wire all 57 widgets to use the trait; migrate the 16 hand-rolled in_affix
implementations to shared sidebar_affix (fixes 6 latent bugs where
in_affix was never persisted). SearchWidget intentionally skips the shared
form section (its business show_type collides) but adopts the affix helper.
- widget_display_callback parity verified via tmp assertion script.
Refs .specs/plans/zibll-widgets-1to1.md Phase 2 (US1/US5)
Co-Authored-By: Claude Fable 5
---
.specs/plans/zibll-widgets-1to1.md | 24 +-
inc/modules/widget-common.php | 638 ++++++++++++++++++
inc/modules/widgets-register.php | 210 +-----
src/AKSRT/Widgets/AboutWidget.php | 11 +-
src/AKSRT/Widgets/ButtonsWidget.php | 11 +-
src/AKSRT/Widgets/CategoriesWidget.php | 20 +-
src/AKSRT/Widgets/CategoryBlockWidget.php | 11 +-
src/AKSRT/Widgets/CategoryRecommendWidget.php | 11 +-
src/AKSRT/Widgets/CategoryTabsWidget.php | 11 +-
src/AKSRT/Widgets/CustomHtmlGapWidget.php | 11 +-
src/AKSRT/Widgets/FriendLinksWidget.php | 10 +-
src/AKSRT/Widgets/HotPostsWidget.php | 18 +-
src/AKSRT/Widgets/IconCardsWidget.php | 11 +-
src/AKSRT/Widgets/NoticeWidget.php | 21 +-
src/AKSRT/Widgets/RecentCommentsWidget.php | 19 +-
src/AKSRT/Widgets/SearchWidget.php | 30 +-
src/AKSRT/Widgets/TabPostsWidget.php | 9 +-
src/AKSRT/Widgets/TagCloudWidget.php | 19 +-
.../Widgets/Traits/CommonWidgetFields.php | 73 ++
src/AKSRT/Widgets/WidgetAnnouncement.php | 11 +-
src/AKSRT/Widgets/WidgetAuthorCard.php | 17 +-
src/AKSRT/Widgets/WidgetBookmarkLinks.php | 11 +-
src/AKSRT/Widgets/WidgetComboInfo.php | 11 +-
src/AKSRT/Widgets/WidgetCoverCards.php | 11 +-
src/AKSRT/Widgets/WidgetFaq.php | 11 +-
src/AKSRT/Widgets/WidgetFeatureIntro.php | 11 +-
src/AKSRT/Widgets/WidgetForumBoardList.php | 11 +-
src/AKSRT/Widgets/WidgetForumHotTopics.php | 17 +-
src/AKSRT/Widgets/WidgetForumPlateInfo.php | 11 +-
src/AKSRT/Widgets/WidgetForumPlateLists.php | 11 +-
.../Widgets/WidgetForumPlateModerator.php | 8 +-
src/AKSRT/Widgets/WidgetForumRecentTopics.php | 18 +-
src/AKSRT/Widgets/WidgetForumTabTopics.php | 11 +-
src/AKSRT/Widgets/WidgetForumTopicLists.php | 11 +-
src/AKSRT/Widgets/WidgetForumTopicTerms.php | 11 +-
src/AKSRT/Widgets/WidgetHighlights.php | 11 +-
src/AKSRT/Widgets/WidgetIconCoverCards.php | 11 +-
src/AKSRT/Widgets/WidgetIframe.php | 11 +-
src/AKSRT/Widgets/WidgetLangSwitch.php | 11 +-
src/AKSRT/Widgets/WidgetMainPosts.php | 20 +-
src/AKSRT/Widgets/WidgetOnelinePosts.php | 11 +-
src/AKSRT/Widgets/WidgetPaidPosts.php | 17 +-
src/AKSRT/Widgets/WidgetPartnersScroll.php | 11 +-
src/AKSRT/Widgets/WidgetPostPayBox.php | 9 +-
src/AKSRT/Widgets/WidgetPostsNavs.php | 25 +-
src/AKSRT/Widgets/WidgetPricingPlans.php | 11 +-
src/AKSRT/Widgets/WidgetProductList.php | 11 +-
src/AKSRT/Widgets/WidgetProductTab.php | 11 +-
src/AKSRT/Widgets/WidgetReviews.php | 11 +-
src/AKSRT/Widgets/WidgetSlider.php | 11 +-
src/AKSRT/Widgets/WidgetStats.php | 11 +-
src/AKSRT/Widgets/WidgetTermAggregation.php | 11 +-
src/AKSRT/Widgets/WidgetTermCards.php | 11 +-
src/AKSRT/Widgets/WidgetTextTitle.php | 17 +-
src/AKSRT/Widgets/WidgetTiltCases.php | 11 +-
src/AKSRT/Widgets/WidgetUserCard.php | 26 +-
src/AKSRT/Widgets/WidgetUserLists.php | 11 +-
src/AKSRT/Widgets/WidgetUserRanking.php | 35 +-
src/AKSRT/Widgets/WidgetVideoPlayer.php | 11 +-
src/AKSRT/Widgets/WidgetYiyan.php | 20 +-
60 files changed, 1227 insertions(+), 489 deletions(-)
create mode 100644 inc/modules/widget-common.php
create mode 100644 src/AKSRT/Widgets/Traits/CommonWidgetFields.php
diff --git a/.specs/plans/zibll-widgets-1to1.md b/.specs/plans/zibll-widgets-1to1.md
index f511f044..b016a0eb 100644
--- a/.specs/plans/zibll-widgets-1to1.md
+++ b/.specs/plans/zibll-widgets-1to1.md
@@ -30,12 +30,12 @@
### Done when
-- [ ] `inc/helpers/aksrt-template-tags.php` 存在 `function aksrt_render_pagination(?WP_Query $query = null, array $args = []): void`,含 `numbered`(`.pagenav.ajax-pag.aksrt-pagination` + `paginate_links` + 跳转)与 `load_more`/`infinite`(`.aksrt-pagination-ajax` + `data-mode`/`data-query-vars`/`data-render-config`)两分支。
-- [ ] `grep -n "display: block !important" assets/css/frontend/post-card.css` 不再出现在基础 `.posts-item` 规则块(行 28-46);`.posts-item.card` 的 `display: inline-block` 生效,可用浏览器/截图确认 3 列卡片网格(验证命令:`grep -A3 '.posts-item.card {' assets/css/frontend/post-card.css` 见 `display: inline-block`)。
-- [ ] `archive-resources.php` 不再含 `posts-row ajaxpager` 手写容器与 `get_template_part('templates/parts/content', 'post-card')`,改为调用 `aksrt_render_post_list()` + `aksrt_render_pagination()`(`grep -n "aksrt_render_post_list\|get_template_part.*post-card" archive-resources.php` 确认)。
-- [ ] `index.php:40`、`search.php:281`、`archive-resources.php` 的 `aksrt_render_pagination()` 调用所在文件在 `php -l` 下无未定义函数风险(函数已在 `aksrt-template-tags.php` 定义且通过 `inc/bootstrap.php` 加载链可达)。
-- [ ] `php tools/audit-admin-contracts.php` 退出码 0(无新增 schema/REST 硬不一致)。
-- [ ] 全部改动文件 ≤1000 行;超限已拆。
+- [x] `inc/helpers/aksrt-template-tags.php` 存在 `function aksrt_render_pagination(?WP_Query $query = null, array $args = []): void`,含 `numbered`(`.pagenav.ajax-pag.aksrt-pagination` + `paginate_links` + 跳转)与 `load_more`/`infinite`(`.aksrt-pagination-ajax` + `data-mode`/`data-query-vars`/`data-render-config`)两分支。
+- [x] `grep -n "display: block !important" assets/css/frontend/post-card.css` 不再出现在基础 `.posts-item` 规则块(行 28-46);`.posts-item.card` 的 `display: inline-block` 生效,可用浏览器/截图确认 3 列卡片网格(验证命令:`grep -A3 '.posts-item.card {' assets/css/frontend/post-card.css` 见 `display: inline-block`)。
+- [x] `archive-resources.php` 不再含 `posts-row ajaxpager` 手写容器与 `get_template_part('templates/parts/content', 'post-card')`,改为调用 `aksrt_render_post_list()` + `aksrt_render_pagination()`(`grep -n "aksrt_render_post_list\|get_template_part.*post-card" archive-resources.php` 确认)。
+- [x] `index.php:40`、`search.php:281`、`archive-resources.php` 的 `aksrt_render_pagination()` 调用所在文件在 `php -l` 下无未定义函数风险(函数已在 `aksrt-template-tags.php` 定义且通过 `inc/bootstrap.php` 加载链可达)。
+- [x] `php tools/audit-admin-contracts.php` 退出码 0(无新增 schema/REST 硬不一致)。
+- [x] 全部改动文件 ≤1000 行;超限已拆。
---
@@ -49,12 +49,12 @@
### Done when
-- [ ] `inc/modules/widgets-register.php` 的 `aksrt_widget_common_defaults()` / `normalize_common()` / `render_common_form()` 覆盖上述全部字段(`grep -c "subtitle\|title_style\|title_highlight\|title_link\|show_type\|show_id_type\|sidebar_affix\|animation_in\|animation_repeat\|layout_bg" inc/modules/widgets-register.php` ≥ 预期命中)。
-- [ ] 新增 `src/AKSRT/Widgets/Traits/CommonWidgetFields.php`(trait),封装 `render_common_form_section()` + `update_common_fields()`,`php -l` 通过。
-- [ ] `grep -rL "CommonWidgetFields\|aksrt_widget_render_common_form" src/AKSRT/Widgets/*.php` 在所有**非纯展示/无表单**小工具上为空(即全部接入)。
-- [ ] `grep -rln "in_affix" src/AKSRT/Widgets/*.php` 返回的文件数 = 0(局部重复实现已删除,逻辑全部经共享层 `sidebar_affix`)。
-- [ ] `aksrt_filter_widget_display_by_area()`(`widget_display_callback`)读取的 `show_type`/`show_id_type`/`show_ids` 字段与共享层 `normalize_common()` 输出一致;任意小工具设置「仅 PC」在移动端 `widget_display_callback` 返回 `false`(可写一次性 PHP 断言脚本置于 `$CLAUDE_JOB_DIR/tmp` 验证,不入库)。
-- [ ] `php tools/audit-admin-contracts.php` 退出码 0;改动文件 ≤1000 行。
+- [x] `inc/modules/widgets-register.php` 的 `aksrt_widget_common_defaults()` / `normalize_common()` / `render_common_form()` 覆盖上述全部字段(`grep -c "subtitle\|title_style\|title_highlight\|title_link\|show_type\|show_id_type\|sidebar_affix\|animation_in\|animation_repeat\|layout_bg" inc/modules/widgets-register.php` ≥ 预期命中)。—— 实现已抽至 `inc/modules/widget-common.php`(由 widgets-register.php require),token 命中 115。
+- [x] 新增 `src/AKSRT/Widgets/Traits/CommonWidgetFields.php`(trait),封装 `render_common_form_section()` + `update_common_fields()`,`php -l` 通过。
+- [x] `grep -rL "CommonWidgetFields\|aksrt_widget_render_common_form" src/AKSRT/Widgets/*.php` 在所有**非纯展示/无表单**小工具上为空(即全部接入)。—— 全部 57 个小工具接入(WidgetProductOneline 经继承);SearchWidget 因业务 `show_type` 与共享 `show_type` 同名冲突,刻意不渲染共享表单段(已在代码注释说明),但已 `use` trait 并接入 `update_common_fields` + `aksrt_widget_affix_attrs`。
+- [x] `grep -rln "in_affix" src/AKSRT/Widgets/*.php` 返回的文件数 = 0(局部重复实现已删除,逻辑全部经共享层 `sidebar_affix`)。
+- [x] `aksrt_filter_widget_display_by_area()`(`widget_display_callback`)读取的 `show_type`/`show_id_type`/`show_ids` 字段与共享层 `normalize_common()` 输出一致;任意小工具设置「仅 PC」在移动端 `widget_display_callback` 返回 `false`(可写一次性 PHP 断言脚本置于 `$CLAUDE_JOB_DIR/tmp` 验证,不入库)。—— 断言脚本 `$CLAUDE_JOB_DIR/tmp/assert-display-parity.php` 全部通过(含 only_pc/only_sm/show/hide/legacy pc·include·exclude·in_affix 迁移)。
+- [x] `php tools/audit-admin-contracts.php` 退出码 0;改动文件 ≤1000 行。
---
diff --git a/inc/modules/widget-common.php b/inc/modules/widget-common.php
new file mode 100644
index 00000000..c26baba8
--- /dev/null
+++ b/inc/modules/widget-common.php
@@ -0,0 +1,638 @@
+
+ */
+ function aksrt_widget_common_defaults(): array {
+ return [
+ 'subtitle' => '',
+ 'title_style' => '',
+ 'title_highlight' => '',
+ 'title_highlight_color'=> 'focus-color',
+ 'title_link' => ['url' => '', 'text' => '', 'target' => ''],
+ 'title_link2' => ['url' => '', 'text' => '', 'target' => ''],
+ 'title_link3' => ['url' => '', 'text' => '', 'target' => ''],
+ 'title_link_style' => ['color_1' => 'jb-blue', 'color_2' => 'jb-yellow', 'color_3' => 'jb-green'],
+ 'sidebar_affix' => false,
+ 'show_type' => 'all',
+ 'show_id_type' => '',
+ 'show_ids' => '',
+ 'animation_in' => '',
+ 'animation_repeat' => false,
+ 'layout_pin_pc' => ['top' => '', 'bottom' => ''],
+ 'layout_pin_m' => ['top' => '', 'bottom' => ''],
+ 'layout_bg' => ['img_white' => [], 'img_dark' => []],
+ ];
+ }
+}
+
+if (!function_exists('aksrt_widget_normalize_link')) {
+ /**
+ * Normalize a title_linkN array {url,text,target}.
+ *
+ * @param mixed $raw
+ * @return array{url:string,text:string,target:string}
+ */
+ function aksrt_widget_normalize_link($raw): array {
+ $raw = is_array($raw) ? $raw : [];
+ return [
+ 'url' => esc_url_raw(trim((string) ($raw['url'] ?? ''))),
+ 'text' => sanitize_text_field((string) ($raw['text'] ?? '')),
+ 'target' => ($raw['target'] ?? '') === '_blank' ? '_blank' : '',
+ ];
+ }
+}
+
+if (!function_exists('aksrt_widget_normalize_common')) {
+ /**
+ * Normalize common widget instance fields, filling defaults for legacy instances.
+ *
+ * Accepts both the new structured shape (title_link as array) and legacy
+ * flat shapes (title_link as URL string, in_affix bool) emitted by older
+ * widget versions, normalizing them onto the canonical shared schema.
+ *
+ * @param array $instance
+ * @return array
+ */
+ function aksrt_widget_normalize_common(array $instance): array {
+ $defaults = aksrt_widget_common_defaults();
+ $result = array_merge($defaults, $instance);
+
+ $result['subtitle'] = sanitize_text_field($result['subtitle'] ?? '');
+
+ $result['title_style'] = in_array($result['title_style'] ?? '', ['', 'big'], true) ? $result['title_style'] : '';
+ $result['title_highlight'] = sanitize_text_field($result['title_highlight'] ?? '');
+ $result['title_highlight_color'] = sanitize_html_class((string) ($result['title_highlight_color'] ?? 'focus-color'));
+ if ($result['title_highlight_color'] === '') {
+ $result['title_highlight_color'] = 'focus-color';
+ }
+
+ // Legacy flat title_link (URL string) → structured first link.
+ if (is_string($result['title_link'] ?? null)) {
+ $result['title_link'] = ['url' => esc_url_raw(trim($result['title_link'])), 'text' => '', 'target' => ''];
+ }
+ $result['title_link'] = aksrt_widget_normalize_link($result['title_link'] ?? []);
+ $result['title_link2'] = aksrt_widget_normalize_link($result['title_link2'] ?? []);
+ $result['title_link3'] = aksrt_widget_normalize_link($result['title_link3'] ?? []);
+
+ $tl_style = is_array($result['title_link_style'] ?? null) ? $result['title_link_style'] : [];
+ $result['title_link_style'] = [
+ 'color_1' => sanitize_html_class((string) ($tl_style['color_1'] ?? 'jb-blue')) ?: 'jb-blue',
+ 'color_2' => sanitize_html_class((string) ($tl_style['color_2'] ?? 'jb-yellow')) ?: 'jb-yellow',
+ 'color_3' => sanitize_html_class((string) ($tl_style['color_3'] ?? 'jb-green')) ?: 'jb-green',
+ ];
+
+ // show_type: accept both AKSRT legacy (pc/mobile) and Zibll (only_pc/only_sm) tokens.
+ $show_type = (string) ($result['show_type'] ?? 'all');
+ if ($show_type === 'only_pc' || $show_type === 'pc') {
+ $show_type = 'only_pc';
+ } elseif ($show_type === 'only_sm' || $show_type === 'mobile') {
+ $show_type = 'only_sm';
+ } else {
+ $show_type = 'all';
+ }
+ $result['show_type'] = $show_type;
+
+ // show_id_type: Zibll (show/hide) ↔ AKSRT legacy (include/exclude).
+ $id_type = (string) ($result['show_id_type'] ?? '');
+ if ($id_type === 'include') {
+ $id_type = 'show';
+ } elseif ($id_type === 'exclude') {
+ $id_type = 'hide';
+ } elseif (!in_array($id_type, ['', 'show', 'hide'], true)) {
+ $id_type = '';
+ }
+ $result['show_id_type'] = $id_type;
+ $result['show_ids'] = sanitize_text_field($result['show_ids'] ?? '');
+
+ $result['sidebar_affix'] = !empty($result['sidebar_affix']) || !empty($result['in_affix'] ?? false);
+
+ $result['animation_in'] = sanitize_key($result['animation_in'] ?? '');
+ $result['animation_repeat'] = !empty($result['animation_repeat']);
+
+ foreach (['layout_pin_pc', 'layout_pin_m'] as $pin_key) {
+ $pin = is_array($result[$pin_key] ?? null) ? $result[$pin_key] : [];
+ $result[$pin_key] = [
+ 'top' => preg_match('/^-?\d+$/', (string) ($pin['top'] ?? '')) ? (string) (int) $pin['top'] : '',
+ 'bottom' => preg_match('/^-?\d+$/', (string) ($pin['bottom'] ?? '')) ? (string) (int) $pin['bottom'] : '',
+ ];
+ }
+
+ $bg = is_array($result['layout_bg'] ?? null) ? $result['layout_bg'] : [];
+ $result['layout_bg'] = [
+ 'img_white' => aksrt_widget_normalize_bg($bg['img_white'] ?? []),
+ 'img_dark' => aksrt_widget_normalize_bg($bg['img_dark'] ?? []),
+ ];
+
+ return $result;
+ }
+}
+
+if (!function_exists('aksrt_widget_normalize_bg')) {
+ /**
+ * Normalize a layout_bg sub-block (background CSS props).
+ *
+ * @param mixed $raw
+ * @return array
+ */
+ function aksrt_widget_normalize_bg($raw): array {
+ $raw = is_array($raw) ? $raw : [];
+ $out = [];
+ foreach (['background-color', 'background-image', 'background-position', 'background-repeat', 'background-attachment', 'background-size', 'background-origin'] as $k) {
+ $v = trim((string) ($raw[$k] ?? ''));
+ if ($k === 'background-image' && $v !== '') {
+ $v = esc_url_raw($v);
+ } elseif ($k === 'background-color' && $v !== '') {
+ // Allow hex / rgb / rgba / named; strip tags defensively.
+ $v = sanitize_text_field($v);
+ } else {
+ $v = sanitize_text_field($v);
+ }
+ if ($v !== '') {
+ $out[$k] = $v;
+ }
+ }
+ return $out;
+ }
+}
+
+if (!function_exists('aksrt_widget_animation_options')) {
+ /**
+ * Zibll-aligned module entrance animation options.
+ *
+ * @return array
+ */
+ function aksrt_widget_animation_options(): array {
+ return [
+ '' => __('无动画', 'aksrt'),
+ 'fade' => __('淡入', 'aksrt'),
+ 'slideup' => __('从下往上滑出', 'aksrt'),
+ 'slidedown' => __('从上往下滑出', 'aksrt'),
+ 'slideright' => __('从左往右滑出', 'aksrt'),
+ 'slideleft' => __('从右往左滑出', 'aksrt'),
+ 'zoomin' => __('由小变大', 'aksrt'),
+ 'zoomout' => __('由大变小', 'aksrt'),
+ ];
+ }
+}
+
+if (!function_exists('aksrt_widget_highlight_color_options')) {
+ /**
+ * Highlight color palette (Zibll zib_palette parity, theme classes).
+ *
+ * @return array
+ */
+ function aksrt_widget_highlight_color_options(): array {
+ return [
+ 'focus-color' => __('主题色', 'aksrt'),
+ 'jb-blue' => __('渐变蓝', 'aksrt'),
+ 'jb-green' => __('渐变绿', 'aksrt'),
+ 'jb-yellow' => __('渐变黄', 'aksrt'),
+ 'jb-orange' => __('渐变橙', 'aksrt'),
+ 'jb-red' => __('渐变红', 'aksrt'),
+ 'jb-purple' => __('渐变紫', 'aksrt'),
+ 'jb-pink' => __('渐变粉', 'aksrt'),
+ 'jb-cyan' => __('渐变青', 'aksrt'),
+ 'c-blue' => __('蓝色', 'aksrt'),
+ 'c-green' => __('绿色', 'aksrt'),
+ 'c-red' => __('红色', 'aksrt'),
+ 'c-yellow' => __('黄色', 'aksrt'),
+ 'c-purple' => __('紫色', 'aksrt'),
+ ];
+ }
+}
+
+if (!function_exists('aksrt_widget_render_link_field')) {
+ /**
+ * Render a single title-link sub-field (url/text/target) for the admin form.
+ *
+ * @param string $base Field base name, e.g. 'title_link'.
+ * @param array $link Normalized link {url,text,target}.
+ * @param string $label Field label.
+ */
+ function aksrt_widget_render_link_field(string $base, array $link, string $label): void {
+ ?>
+
+
+
+
+
+ $instance Current widget instance.
+ */
+ function aksrt_widget_render_common_form(array $instance): void {
+ $instance = aksrt_widget_normalize_common($instance);
+ $pin_pc = $instance['layout_pin_pc'];
+ $pin_m = $instance['layout_pin_m'];
+ $bg_w = $instance['layout_bg']['img_white'];
+ $bg_d = $instance['layout_bg']['img_dark'];
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $new_instance
+ * @param array $old_instance
+ * @return array
+ */
+ function aksrt_widget_update_common(array $new_instance, array $old_instance): array {
+ $merged = array_merge($old_instance, $new_instance);
+ return aksrt_widget_normalize_common($merged);
+ }
+}
+
+if (!function_exists('aksrt_widget_render_common_before')) {
+ /**
+ * Build common wrapper classes/attrs for a widget instance.
+ *
+ * Zibll CSF_Widget::wrap_attributes() parity: show-type class, sidebar
+ * affix data-affix, entrance animation class, layout padding/bg CSS vars.
+ * Call this at the start of widget() before rendering content.
+ *
+ * @param array $args Sidebar args.
+ * @param array $instance Widget instance.
+ * @return array CSS class names (Zibll show_class style).
+ */
+ function aksrt_widget_get_common_wrapper_classes(array $args, array $instance): array {
+ $instance = aksrt_widget_normalize_common($instance);
+ $classes = [];
+
+ // Show type: PC or mobile (Zibll hidden-xs / visible-xs-block).
+ if ($instance['show_type'] === 'only_pc') {
+ $classes[] = 'hidden-xs';
+ } elseif ($instance['show_type'] === 'only_sm') {
+ $classes[] = 'visible-xs-block';
+ }
+
+ // Sidebar affix
+ if ($instance['sidebar_affix']) {
+ $classes[] = 'is-affix';
+ }
+
+ // Animation (Zibll obs-animate ani-*).
+ if ($instance['animation_in'] !== '') {
+ $classes[] = 'obs-animate';
+ $classes[] = 'ani-' . $instance['animation_in'];
+ if ($instance['animation_repeat']) {
+ $classes[] = 'obs-animate-repeat';
+ }
+ }
+
+ return $classes;
+ }
+}
+
+if (!function_exists('aksrt_widget_common_wrapper_attr')) {
+ /**
+ * Build the full wrapper attribute string (class + style + data-affix) for
+ * the Zibll .zib-widget-wrap parity layer.
+ *
+ * Combine with a widget-specific class via a leading space, or use as-is.
+ *
+ * @param array $instance Widget instance.
+ * @param string $extra_class Additional class (widget-specific).
+ * @return string Escaped attribute string beginning with a leading space, e.g. ' class="…" style="…" data-affix="true"'.
+ */
+ function aksrt_widget_common_wrapper_attr(array $instance, string $extra_class = ''): string {
+ $instance = aksrt_widget_normalize_common($instance);
+ $classes = ['zib-widget-wrap'];
+ foreach (aksrt_widget_get_common_wrapper_classes([], $instance) as $c) {
+ $classes[] = $c;
+ }
+ $extra_class = trim($extra_class);
+ if ($extra_class !== '') {
+ $classes[] = $extra_class;
+ }
+
+ $styles = [];
+ if ($instance['layout_pin_pc']['top'] !== '') {
+ $styles[] = '--pt-pc:' . (int) $instance['layout_pin_pc']['top'] . 'px;';
+ }
+ if ($instance['layout_pin_pc']['bottom'] !== '') {
+ $styles[] = '--pb-pc:' . (int) $instance['layout_pin_pc']['bottom'] . 'px;';
+ }
+ if ($instance['layout_pin_m']['top'] !== '') {
+ $styles[] = '--pt-m:' . (int) $instance['layout_pin_m']['top'] . 'px;';
+ }
+ if ($instance['layout_pin_m']['bottom'] !== '') {
+ $styles[] = '--pb-m:' . (int) $instance['layout_pin_m']['bottom'] . 'px;';
+ }
+ foreach (['white' => 'img_white', 'dark' => 'img_dark'] as $mode => $bg_key) {
+ $bg = $instance['layout_bg'][$bg_key] ?? [];
+ if (!empty($bg['background-color'])) {
+ $styles[] = '--bg-color-' . $mode . ':' . $bg['background-color'] . ';';
+ }
+ if (!empty($bg['background-image'])) {
+ $styles[] = '--bg-img-' . $mode . ':url("' . $bg['background-image'] . '");';
+ }
+ }
+
+ $attr = ' class="' . esc_attr(implode(' ', $classes)) . '"';
+ if ($styles) {
+ $attr .= ' style="' . esc_attr(implode('', $styles)) . '"';
+ }
+ if ($instance['sidebar_affix']) {
+ $attr .= ' data-affix="true"';
+ }
+ return $attr;
+ }
+}
+
+if (!function_exists('aksrt_widget_affix_attrs')) {
+ /**
+ * Compact affix class+attribute helper for widgets that apply affix onto an
+ * inner wrapper (the two-family pattern from the legacy in_affix widgets).
+ *
+ * @param array $instance Widget instance.
+ * @return array{class:string,attr:string} class includes leading space when active; attr includes leading space.
+ */
+ function aksrt_widget_affix_attrs(array $instance): array {
+ $instance = aksrt_widget_normalize_common($instance);
+ if (empty($instance['sidebar_affix'])) {
+ return ['class' => '', 'attr' => ''];
+ }
+ return ['class' => ' is-affix', 'attr' => ' data-affix="true"'];
+ }
+}
+
+if (!function_exists('aksrt_widget_render_title')) {
+ /**
+ * Render the Zibll-aligned widget title block (CSF_Widget::show_title parity).
+ *
+ * Two styles:
+ * - '' (简约): .box-body notop > .title-theme with optional subtitle + more link
+ * - 'big' (居中大标题): .big-title-wrap with up to 3 button links
+ *
+ * Highlight wraps the matched substring in a colored span.
+ *
+ * @param array $instance Widget instance.
+ * @return string Title HTML (empty when no title).
+ */
+ function aksrt_widget_render_title(array $instance): string {
+ $instance = aksrt_widget_normalize_common($instance);
+ $title = trim((string) ($instance['title'] ?? ''));
+ if ($title === '') {
+ return '';
+ }
+
+ $highlight = trim((string) $instance['title_highlight']);
+ $subtitle = (string) $instance['subtitle'];
+ $anim = function_exists('aksrt_widget_animation_class')
+ ? aksrt_widget_animation_class($instance)
+ : '';
+
+ $title = esc_html($title);
+
+ if ($highlight !== '' && $highlight !== $title) {
+ $color = $instance['title_highlight_color'];
+ $hl_esc = esc_html($highlight);
+ $title = str_replace($hl_esc, '' . $hl_esc . '', $title);
+ }
+
+ if ($instance['title_style'] === 'big') {
+ $style = $instance['title_link_style'];
+ $links = '';
+ foreach (['title_link' => 'color_1', 'title_link2' => 'color_2', 'title_link3' => 'color_3'] as $key => $color_key) {
+ $link = $instance[$key];
+ if ($link['url'] !== '' && $link['text'] !== '') {
+ $blank = $link['target'] === '_blank' ? ' target="_blank"' : '';
+ $links .= '' . esc_html($link['text']) . '';
+ }
+ }
+ $links = $links !== '' ? '' . $links . '
' : '';
+ $sub = $subtitle !== '' ? '' . esc_html($subtitle) . '
' : '';
+ return '' . $title . '
' . $sub . $links . '
';
+ }
+
+ // Simple style.
+ $link = $instance['title_link'];
+ $sub = $subtitle !== '' ? '' . esc_html($subtitle) . '' : '';
+ $more = '';
+ if ($link['url'] !== '') {
+ $text = $link['text'] !== '' ? $link['text'] : '' . __('更多', 'aksrt');
+ $blank = $link['target'] === '_blank' ? ' target="_blank"' : '';
+ $more = '';
+ }
+ return '' . $title . $sub . $more . '
';
+ }
+}
+
+if (!function_exists('aksrt_widget_animation_class')) {
+ /**
+ * Zibll CSF_Widget::animation_class parity: obs-animate ani-* [+repeat].
+ *
+ * @param array $instance Normalized widget instance.
+ * @param bool $is_title Reserved for title-specific animation.
+ * @return string
+ */
+ function aksrt_widget_animation_class(array $instance, bool $is_title = false): string {
+ $animation = (string) ($instance['animation_in'] ?? '');
+ if ($animation === '') {
+ return '';
+ }
+ $class = ' obs-animate ani-' . $animation;
+ if (!empty($instance['animation_repeat'])) {
+ $class .= ' obs-animate-repeat';
+ }
+ return $class;
+ }
+}
+
+if (!function_exists('aksrt_widget_should_display_by_ids')) {
+ /**
+ * Check if the current post/page should display the widget based on show_id_type and show_ids.
+ *
+ * show_id_type tokens: '' (no limit) | 'show' (whitelist) | 'hide' (blacklist).
+ * Accepts legacy 'include'/'exclude' via normalize_common.
+ *
+ * @param array $instance Widget instance.
+ * @return bool
+ */
+ function aksrt_widget_should_display_by_ids(array $instance): bool {
+ $instance = aksrt_widget_normalize_common($instance);
+
+ if ($instance['show_id_type'] === '' || $instance['show_ids'] === '') {
+ return true;
+ }
+
+ $current_id = get_the_ID();
+ if (!$current_id) {
+ return true; // Not on a singular page, allow display
+ }
+
+ // Zibll splits on comma/Chinese-comma/whitespace/newline.
+ $ids = array_filter(array_map('absint', preg_split('/[,,\s]+/', $instance['show_ids']) ?: []));
+
+ if ($instance['show_id_type'] === 'show') {
+ return in_array($current_id, $ids, true);
+ }
+
+ if ($instance['show_id_type'] === 'hide') {
+ return !in_array($current_id, $ids, true);
+ }
+
+ return true;
+ }
+}
diff --git a/inc/modules/widgets-register.php b/inc/modules/widgets-register.php
index 3b9ab49b..b0d5145f 100644
--- a/inc/modules/widgets-register.php
+++ b/inc/modules/widgets-register.php
@@ -7,6 +7,8 @@
defined('ABSPATH') || exit;
+require_once AKSRT_DIR . '/inc/modules/widget-common.php';
+
/**
* Register custom widgets.
*/
@@ -307,208 +309,6 @@ if (!function_exists('aksrt_widget_get_area_context')) {
}
}
-// ============================================================
-// Shared Widget Form Helper — 公共表单字段与显示规则
-// ============================================================
-
-if (!function_exists('aksrt_widget_common_defaults')) {
- /**
- * Default values for shared widget instance fields.
- *
- * @return array
- */
- function aksrt_widget_common_defaults(): array {
- return [
- 'subtitle' => '',
- 'title_link' => '',
- 'show_type' => 'all',
- 'show_id_type' => '',
- 'show_ids' => '',
- 'sidebar_affix' => false,
- 'animation_in' => '',
- 'animation_repeat'=> false,
- ];
- }
-}
-
-if (!function_exists('aksrt_widget_normalize_common')) {
- /**
- * Normalize common widget instance fields, filling defaults for legacy instances.
- *
- * @param array $instance
- * @return array
- */
- function aksrt_widget_normalize_common(array $instance): array {
- $defaults = aksrt_widget_common_defaults();
- $result = array_merge($defaults, $instance);
-
- $result['subtitle'] = sanitize_text_field($result['subtitle'] ?? '');
- $result['title_link'] = esc_url_raw($result['title_link'] ?? '');
- $result['show_type'] = in_array($result['show_type'] ?? '', ['all', 'pc', 'mobile'], true) ? $result['show_type'] : 'all';
- $result['show_id_type'] = in_array($result['show_id_type'] ?? '', ['', 'include', 'exclude'], true) ? $result['show_id_type'] : '';
- $result['show_ids'] = sanitize_text_field($result['show_ids'] ?? '');
- $result['sidebar_affix'] = !empty($result['sidebar_affix']);
- $result['animation_in'] = sanitize_key($result['animation_in'] ?? '');
- $result['animation_repeat']= !empty($result['animation_repeat']);
-
- return $result;
- }
-}
-
-if (!function_exists('aksrt_widget_render_common_form')) {
- /**
- * Render shared widget admin form fields.
- *
- * Call this from your widget's form() method after rendering custom fields.
- *
- * @param array $instance Current widget instance.
- */
- function aksrt_widget_render_common_form(array $instance): void {
- $instance = aksrt_widget_normalize_common($instance);
- ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- $new_instance
- * @param array $old_instance
- * @return array
- */
- function aksrt_widget_update_common(array $new_instance, array $old_instance): array {
- $merged = array_merge($old_instance, $new_instance);
- return aksrt_widget_normalize_common($merged);
- }
-}
-
-if (!function_exists('aksrt_widget_render_common_before')) {
- /**
- * Render common wrapper attributes and classes before widget content.
- *
- * Outputs CSS classes for animation, affix, show_type, etc.
- * Call this at the start of widget() before rendering content.
- *
- * @param array $args Sidebar args.
- * @param array $instance Widget instance.
- * @return array Modified args with extra classes.
- */
- function aksrt_widget_get_common_wrapper_classes(array $args, array $instance): array {
- $instance = aksrt_widget_normalize_common($instance);
- $classes = [];
-
- // Show type: PC or mobile
- if ($instance['show_type'] === 'pc') {
- $classes[] = 'aksrt-widget--pc-only';
- } elseif ($instance['show_type'] === 'mobile') {
- $classes[] = 'aksrt-widget--mobile-only';
- }
-
- // Sidebar affix
- if ($instance['sidebar_affix']) {
- $classes[] = 'aksrt-widget--affix';
- }
-
- // Animation
- if ($instance['animation_in'] !== '') {
- $classes[] = 'aksrt-widget--animated';
- $classes[] = 'aksrt-anim-' . $instance['animation_in'];
- if ($instance['animation_repeat']) {
- $classes[] = 'aksrt-anim-repeat';
- }
- }
-
- return $classes;
- }
-}
-
-if (!function_exists('aksrt_widget_should_display_by_ids')) {
- /**
- * Check if the current post/page should display the widget based on show_id_type and show_ids.
- *
- * @param array $instance Widget instance.
- * @return bool
- */
- function aksrt_widget_should_display_by_ids(array $instance): bool {
- $instance = aksrt_widget_normalize_common($instance);
-
- if ($instance['show_id_type'] === '' || $instance['show_ids'] === '') {
- return true;
- }
-
- $current_id = get_the_ID();
- if (!$current_id) {
- return true; // Not on a singular page, allow display
- }
-
- $ids = array_filter(array_map('absint', explode(',', $instance['show_ids'])));
-
- if ($instance['show_id_type'] === 'include') {
- return in_array($current_id, $ids, true);
- }
-
- if ($instance['show_id_type'] === 'exclude') {
- return !in_array($current_id, $ids, true);
- }
-
- return true;
- }
-}
-
if (!function_exists('aksrt_widget_render_location_help')) {
/**
* Render placement hint inside widget admin forms.
@@ -559,11 +359,11 @@ if (!function_exists('aksrt_filter_widget_display_by_area')) {
if (is_array($instance) && function_exists('aksrt_widget_normalize_common')) {
$normalized = aksrt_widget_normalize_common($instance);
- // Show type: PC / mobile filter
- if ($normalized['show_type'] === 'pc' && wp_is_mobile()) {
+ // Show type: PC / mobile filter (Zibll only_pc / only_sm).
+ if ($normalized['show_type'] === 'only_pc' && wp_is_mobile()) {
return false;
}
- if ($normalized['show_type'] === 'mobile' && !wp_is_mobile()) {
+ if ($normalized['show_type'] === 'only_sm' && !wp_is_mobile()) {
return false;
}
diff --git a/src/AKSRT/Widgets/AboutWidget.php b/src/AKSRT/Widgets/AboutWidget.php
index 2de84c6b..ef9241b4 100644
--- a/src/AKSRT/Widgets/AboutWidget.php
+++ b/src/AKSRT/Widgets/AboutWidget.php
@@ -5,9 +5,13 @@
namespace AKSRT\Widgets;
+use AKSRT\Widgets\Traits\CommonWidgetFields;
+
defined('ABSPATH') || exit;
class AboutWidget extends \WP_Widget {
+ use CommonWidgetFields;
+
public function __construct() {
parent::__construct(
@@ -367,12 +371,13 @@ class AboutWidget extends \WP_Widget {
render_common_form_section($instance);
+}
public function update($new_instance, $old_instance): array {
- return [
+ $business = [
'title' => sanitize_text_field($new_instance['title'] ?? ''),
'cover' => esc_url_raw($new_instance['cover'] ?? ''),
- ];
+ ]; return $this->update_common_fields($new_instance, $old_instance, $business);
}
}
diff --git a/src/AKSRT/Widgets/ButtonsWidget.php b/src/AKSRT/Widgets/ButtonsWidget.php
index 98ddbfc7..5451e8dc 100644
--- a/src/AKSRT/Widgets/ButtonsWidget.php
+++ b/src/AKSRT/Widgets/ButtonsWidget.php
@@ -7,9 +7,13 @@
namespace AKSRT\Widgets;
+use AKSRT\Widgets\Traits\CommonWidgetFields;
+
defined('ABSPATH') || exit;
class ButtonsWidget extends \WP_Widget {
+ use CommonWidgetFields;
+
public function __construct() {
parent::__construct('aksrt_buttons', esc_html__('AKSRT 按钮组', 'aksrt'), [
@@ -213,7 +217,8 @@ class ButtonsWidget extends \WP_Widget {
render_common_form_section($instance);
+}
/**
* @param array $new_instance
@@ -244,7 +249,7 @@ class ButtonsWidget extends \WP_Widget {
}
}
}
- return [
+ $business = [
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
'align' => in_array(($new_instance['align'] ?? ''), ['left', 'center', 'right'], true) ? $new_instance['align'] : 'center',
'radius' => !empty($new_instance['radius']) ? 1 : 0,
@@ -256,6 +261,6 @@ class ButtonsWidget extends \WP_Widget {
'obs_animation' => sanitize_key((string) ($new_instance['obs_animation'] ?? '')),
'animation_repeat'=> !empty($new_instance['animation_repeat']) ? 1 : 0,
'buttons' => $buttons,
- ];
+ ]; return $this->update_common_fields($new_instance, $old_instance, $business);
}
}
diff --git a/src/AKSRT/Widgets/CategoriesWidget.php b/src/AKSRT/Widgets/CategoriesWidget.php
index bf0904d5..e779def6 100644
--- a/src/AKSRT/Widgets/CategoriesWidget.php
+++ b/src/AKSRT/Widgets/CategoriesWidget.php
@@ -7,9 +7,12 @@
namespace AKSRT\Widgets;
+use AKSRT\Widgets\Traits\CommonWidgetFields;
+
defined('ABSPATH') || exit;
class CategoriesWidget extends \WP_Widget {
+ use CommonWidgetFields;
public function __construct() {
parent::__construct(
@@ -176,12 +179,12 @@ class CategoriesWidget extends \WP_Widget {
if ($title !== '') {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
- $in_affix = !empty($instance['in_affix']);
- if ($in_affix) {
- echo '';
+ $affix = function_exists('aksrt_widget_affix_attrs') ? aksrt_widget_affix_attrs($instance) : ['class' => '', 'attr' => ''];
+ if ($affix['class'] !== '') {
+ echo '
';
}
echo self::render_html($instance); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
- if ($in_affix) {
+ if ($affix['class'] !== '') {
echo '
';
}
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
@@ -238,9 +241,7 @@ class CategoriesWidget extends \WP_Widget {
-
-
-
+ render_common_form_section($instance); ?>
update_common_fields($new_instance, $old_instance, [
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
'count' => max(1, min(50, (int) ($new_instance['count'] ?? 8))),
'orderby' => $orderby,
@@ -267,7 +268,6 @@ class CategoriesWidget extends \WP_Widget {
'show_thumb' => !empty($new_instance['show_thumb']) ? 1 : 0,
'hide_empty' => !empty($new_instance['hide_empty']) ? 1 : 0,
'hierarchical' => !empty($new_instance['hierarchical']) ? 1 : 0,
- 'in_affix' => !empty($new_instance['in_affix']) ? 1 : 0,
- ];
+ ]);
}
}
diff --git a/src/AKSRT/Widgets/CategoryBlockWidget.php b/src/AKSRT/Widgets/CategoryBlockWidget.php
index 953c6773..9a466718 100644
--- a/src/AKSRT/Widgets/CategoryBlockWidget.php
+++ b/src/AKSRT/Widgets/CategoryBlockWidget.php
@@ -5,6 +5,8 @@
namespace AKSRT\Widgets;
+use AKSRT\Widgets\Traits\CommonWidgetFields;
+
defined('ABSPATH') || exit;
/**
@@ -311,6 +313,8 @@ if (!function_exists('aksrt_home_widget_get_lazyload_args')) {
* Homepage category block widget.
*/
class CategoryBlockWidget extends \WP_Widget {
+ use CommonWidgetFields;
+
public function __construct() {
parent::__construct(
@@ -486,10 +490,11 @@ class CategoryBlockWidget extends \WP_Widget {
render_common_form_section($instance);
+}
public function update($new_instance, $old_instance): array {
- return [
+ $business = [
'title' => sanitize_text_field($new_instance['title'] ?? ''),
'category_id' => absint($new_instance['category_id'] ?? 0),
'count' => max(1, min(12, absint($new_instance['count'] ?? 6))),
@@ -497,7 +502,7 @@ class CategoryBlockWidget extends \WP_Widget {
'layout' => aksrt_home_widget_resolve_layout($new_instance['layout'] ?? 'card', 'card'),
'show_more' => ! empty($new_instance['show_more']) ? 1 : 0,
'paginate' => in_array(sanitize_key((string) ($new_instance['paginate'] ?? 'none')), ['none', 'ajax', 'number'], true) ? sanitize_key((string) ($new_instance['paginate'] ?? 'none')) : 'none',
- ];
+ ]; return $this->update_common_fields($new_instance, $old_instance, $business);
}
public static function resolve_category_id_static(array $instance): int {
diff --git a/src/AKSRT/Widgets/CategoryRecommendWidget.php b/src/AKSRT/Widgets/CategoryRecommendWidget.php
index 32720184..88b8a3ac 100644
--- a/src/AKSRT/Widgets/CategoryRecommendWidget.php
+++ b/src/AKSRT/Widgets/CategoryRecommendWidget.php
@@ -5,12 +5,16 @@
namespace AKSRT\Widgets;
+use AKSRT\Widgets\Traits\CommonWidgetFields;
+
defined('ABSPATH') || exit;
/**
* Homepage category recommend cards widget.
*/
class CategoryRecommendWidget extends \WP_Widget {
+ use CommonWidgetFields;
+
public function __construct() {
parent::__construct(
@@ -194,10 +198,11 @@ class CategoryRecommendWidget extends \WP_Widget {
render_common_form_section($instance);
+}
public function update($new_instance, $old_instance): array {
- return [
+ $business = [
'title' => sanitize_text_field($new_instance['title'] ?? ''),
'category_ids' => aksrt_home_widget_normalize_category_ids($new_instance['category_ids'] ?? [], 4),
'posts_per_category' => max(1, min(8, absint($new_instance['posts_per_category'] ?? 5))),
@@ -206,7 +211,7 @@ class CategoryRecommendWidget extends \WP_Widget {
'show_description' => ! empty($new_instance['show_description']) ? 1 : 0,
'show_dates' => ! empty($new_instance['show_dates']) ? 1 : 0,
'show_more' => ! empty($new_instance['show_more']) ? 1 : 0,
- ];
+ ]; return $this->update_common_fields($new_instance, $old_instance, $business);
}
/**
diff --git a/src/AKSRT/Widgets/CategoryTabsWidget.php b/src/AKSRT/Widgets/CategoryTabsWidget.php
index f279e824..2ebb625e 100644
--- a/src/AKSRT/Widgets/CategoryTabsWidget.php
+++ b/src/AKSRT/Widgets/CategoryTabsWidget.php
@@ -5,12 +5,16 @@
namespace AKSRT\Widgets;
+use AKSRT\Widgets\Traits\CommonWidgetFields;
+
defined('ABSPATH') || exit;
/**
* Homepage category tabs widget.
*/
class CategoryTabsWidget extends \WP_Widget {
+ use CommonWidgetFields;
+
public function __construct() {
parent::__construct(
@@ -157,17 +161,18 @@ public function form($instance): void {
render_common_form_section($instance);
+}
public function update($new_instance, $old_instance): array {
- return [
+ $business = [
'title' => sanitize_text_field($new_instance['title'] ?? ''),
'category_ids' => aksrt_home_widget_normalize_category_ids($new_instance['category_ids'] ?? [], 6),
'count' => max(1, min(12, absint($new_instance['count'] ?? 6))),
'columns' => max(1, min(4, absint($new_instance['columns'] ?? 3))),
'layout' => aksrt_home_widget_resolve_layout($new_instance['layout'] ?? 'card', 'card'),
'show_more' => ! empty($new_instance['show_more']) ? 1 : 0,
- ];
+ ]; return $this->update_common_fields($new_instance, $old_instance, $business);
}
/**
diff --git a/src/AKSRT/Widgets/CustomHtmlGapWidget.php b/src/AKSRT/Widgets/CustomHtmlGapWidget.php
index 55ab4486..fb4e11bb 100644
--- a/src/AKSRT/Widgets/CustomHtmlGapWidget.php
+++ b/src/AKSRT/Widgets/CustomHtmlGapWidget.php
@@ -7,9 +7,13 @@
namespace AKSRT\Widgets;
+use AKSRT\Widgets\Traits\CommonWidgetFields;
+
defined('ABSPATH') || exit;
class CustomHtmlGapWidget extends \WP_Widget {
+ use CommonWidgetFields;
+
public function __construct() {
parent::__construct('aksrt_custom_html_gap', esc_html__('AKSRT 自定义HTML/间距', 'aksrt'), [
@@ -90,7 +94,8 @@ class CustomHtmlGapWidget extends \WP_Widget {
render_common_form_section($instance);
+}
/**
* @param array
$new_instance
@@ -98,13 +103,13 @@ class CustomHtmlGapWidget extends \WP_Widget {
* @return array
*/
public function update($new_instance, $old_instance): array {
- return [
+ $business = [
'mode' => in_array(($new_instance['mode'] ?? ''), ['html', 'gap'], true) ? $new_instance['mode'] : 'html',
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
'content' => wp_kses_post((string) ($new_instance['content'] ?? '')),
'gap_height' => max(0, min(500, (int) ($new_instance['gap_height'] ?? 20))),
'gap_height_m' => max(0, min(500, (int) ($new_instance['gap_height_m'] ?? $new_instance['gap_height'] ?? 20))),
'full_width' => !empty($new_instance['full_width']) ? 1 : 0,
- ];
+ ]; return $this->update_common_fields($new_instance, $old_instance, $business);
}
}
diff --git a/src/AKSRT/Widgets/FriendLinksWidget.php b/src/AKSRT/Widgets/FriendLinksWidget.php
index 6818da21..d8824699 100644
--- a/src/AKSRT/Widgets/FriendLinksWidget.php
+++ b/src/AKSRT/Widgets/FriendLinksWidget.php
@@ -7,9 +7,13 @@
namespace AKSRT\Widgets;
+use AKSRT\Widgets\Traits\CommonWidgetFields;
+
defined('ABSPATH') || exit;
class FriendLinksWidget extends \WP_Widget {
+ use CommonWidgetFields;
+
public function __construct() {
parent::__construct('aksrt_friend_links', esc_html__('AKSRT 友情链接', 'aksrt'), [
@@ -155,7 +159,8 @@ class FriendLinksWidget extends \WP_Widget {
render_common_form_section($instance);
+}
/**
* @param array $new_instance
@@ -167,7 +172,7 @@ class FriendLinksWidget extends \WP_Widget {
if (!in_array($type, ['card', 'bigcard', 'image', 'simple'], true)) {
$type = 'card';
}
- return [
+ $business = [
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
'source' => ($new_instance['source'] ?? '') === 'bookmarks' ? 'bookmarks' : 'settings',
'type' => $type,
@@ -183,5 +188,6 @@ class FriendLinksWidget extends \WP_Widget {
'columns' => max(1, min(4, (int) ($new_instance['columns'] ?? 2))),
'show_icon' => !empty($new_instance['show_icon']) ? 1 : 0,
];
+ return $this->update_common_fields($new_instance, $old_instance, $business);
}
}
diff --git a/src/AKSRT/Widgets/HotPostsWidget.php b/src/AKSRT/Widgets/HotPostsWidget.php
index 0a5b5336..f4ae3d2d 100644
--- a/src/AKSRT/Widgets/HotPostsWidget.php
+++ b/src/AKSRT/Widgets/HotPostsWidget.php
@@ -7,9 +7,12 @@
namespace AKSRT\Widgets;
+use AKSRT\Widgets\Traits\CommonWidgetFields;
+
defined('ABSPATH') || exit;
class HotPostsWidget extends \WP_Widget {
+ use CommonWidgetFields;
public function __construct() {
parent::__construct(
@@ -204,12 +207,12 @@ class HotPostsWidget extends \WP_Widget {
if ($title) {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
- $in_affix = !empty($instance['in_affix']);
+ $affix = function_exists('aksrt_widget_affix_attrs') ? aksrt_widget_affix_attrs($instance) : ['class' => '', 'attr' => ''];
$instance['paginate'] = sanitize_key((string) ($instance['paginate'] ?? 'ajax'));
$payload = self::query_payload($instance, 1);
$rest = rest_url('aksrt/v1/widgets/hot-posts');
- $wrap = 'ajaxpager' . ($in_affix ? ' is-affix' : '');
- echo '';
+ $wrap = 'ajaxpager' . $affix['class'];
+ echo '
';
echo $payload['html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $payload['paginate_html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '
';
@@ -246,8 +249,7 @@ class HotPostsWidget extends \WP_Widget {
-
-
+
+ render_common_form_section($instance); ?>
update_common_fields($new_instance, $old_instance, [
'title' => sanitize_text_field($new_instance['title'] ?? ''),
'count' => absint($new_instance['count'] ?? 5),
'order_by' => in_array($new_instance['order_by'] ?? '', ['views', 'comments', 'likes', 'favorites', 'downloads'], true) ? $new_instance['order_by'] : 'views',
'days' => absint($new_instance['days'] ?? 30),
'show_thumb' => !empty($new_instance['show_thumb']) ? 1 : 0,
- 'in_affix' => !empty($new_instance['in_affix']) ? 1 : 0,
'paginate' => in_array(sanitize_key((string) ($new_instance['paginate'] ?? 'ajax')), ['none', 'ajax', 'number'], true) ? sanitize_key((string) ($new_instance['paginate'] ?? 'ajax')) : 'ajax',
- ];
+ ]);
}
}
diff --git a/src/AKSRT/Widgets/IconCardsWidget.php b/src/AKSRT/Widgets/IconCardsWidget.php
index 8bfe13d0..1d573ff5 100644
--- a/src/AKSRT/Widgets/IconCardsWidget.php
+++ b/src/AKSRT/Widgets/IconCardsWidget.php
@@ -7,9 +7,13 @@
namespace AKSRT\Widgets;
+use AKSRT\Widgets\Traits\CommonWidgetFields;
+
defined('ABSPATH') || exit;
class IconCardsWidget extends \WP_Widget {
+ use CommonWidgetFields;
+
public function __construct() {
parent::__construct('aksrt_icon_cards', esc_html__('AKSRT 图标卡片', 'aksrt'), [
@@ -193,7 +197,8 @@ class IconCardsWidget extends \WP_Widget {
" value="" placeholder="">
render_common_form_section($instance);
+}
/**
* @param array
$new_instance
@@ -201,7 +206,7 @@ class IconCardsWidget extends \WP_Widget {
* @return array
*/
public function update($new_instance, $old_instance): array {
- return [
+ $business = [
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
'columns' => max(1, min(6, (int) ($new_instance['columns'] ?? 2))),
'm_row' => max(1, min(4, (int) ($new_instance['m_row'] ?? 2))),
@@ -210,6 +215,6 @@ class IconCardsWidget extends \WP_Widget {
'icon_radius' => !empty($new_instance['icon_radius']) ? 1 : 0,
'obs_animation' => sanitize_key((string) ($new_instance['obs_animation'] ?? '')),
'cards' => $this->normalize($new_instance['cards'] ?? []),
- ];
+ ]; return $this->update_common_fields($new_instance, $old_instance, $business);
}
}
diff --git a/src/AKSRT/Widgets/NoticeWidget.php b/src/AKSRT/Widgets/NoticeWidget.php
index b6490519..a88c67f4 100644
--- a/src/AKSRT/Widgets/NoticeWidget.php
+++ b/src/AKSRT/Widgets/NoticeWidget.php
@@ -7,9 +7,12 @@
namespace AKSRT\Widgets;
+use AKSRT\Widgets\Traits\CommonWidgetFields;
+
defined('ABSPATH') || exit;
class NoticeWidget extends \WP_Widget {
+ use CommonWidgetFields;
public function __construct() {
parent::__construct('aksrt_notice', esc_html__('AKSRT 滚动公告', 'aksrt'), [
@@ -36,7 +39,7 @@ if (!$items) {
$radius = !empty($instance['radius']);
$interval = max(2, min(15, (int) ($instance['interval'] ?? 4)));
$align = sanitize_html_class((string) ($instance['alignment'] ?? ''));
- $in_affix = !empty($instance['in_affix']);
+ $affix = function_exists('aksrt_widget_affix_attrs') ? aksrt_widget_affix_attrs($instance) : ['class' => '', 'attr' => ''];
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
@@ -51,12 +54,9 @@ if (!$items) {
if ($align !== '') {
$wrap_cls .= ' ' . $align;
}
- if ($in_affix) {
- $wrap_cls .= ' is-affix';
- }
+ $wrap_cls .= $affix['class'];
- $attr = $in_affix ? ' data-affix="true"' : '';
- echo '';
+ echo '
';
echo '
';
// Prefer shared vertical new-swiper (zib_notice parity) via slider.js Type C.
@@ -192,14 +192,14 @@ if (!$items) {
-
-
+
+ render_common_form_section($instance); ?>
update_common_fields($new_instance, $old_instance, [
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
'items' => $this->normalize_items(['items' => $new_instance['items'] ?? []]),
'interval' => max(2, min(15, (int) ($new_instance['interval'] ?? 4))),
@@ -207,7 +207,6 @@ if (!$items) {
'alignment' => sanitize_html_class((string) ($new_instance['alignment'] ?? '')),
'blank' => !empty($new_instance['blank']) ? 1 : 0,
'radius' => !empty($new_instance['radius']) ? 1 : 0,
- 'in_affix' => !empty($new_instance['in_affix']) ? 1 : 0,
- ];
+ ]);
}
}
diff --git a/src/AKSRT/Widgets/RecentCommentsWidget.php b/src/AKSRT/Widgets/RecentCommentsWidget.php
index 0e69a951..0f4e2eb7 100644
--- a/src/AKSRT/Widgets/RecentCommentsWidget.php
+++ b/src/AKSRT/Widgets/RecentCommentsWidget.php
@@ -7,9 +7,12 @@
namespace AKSRT\Widgets;
+use AKSRT\Widgets\Traits\CommonWidgetFields;
+
defined('ABSPATH') || exit;
class RecentCommentsWidget extends \WP_Widget {
+ use CommonWidgetFields;
public function __construct() {
parent::__construct('aksrt_recent_comments', esc_html__('AKSRT 最新评论', 'aksrt'), [
@@ -169,12 +172,12 @@ class RecentCommentsWidget extends \WP_Widget {
if ($title !== '') {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
- $in_affix = !empty($instance['in_affix']);
- if ($in_affix) {
- echo '
';
+ $affix = function_exists('aksrt_widget_affix_attrs') ? aksrt_widget_affix_attrs($instance) : ['class' => '', 'attr' => ''];
+ if ($affix['class'] !== '') {
+ echo '
';
}
echo self::render_html($instance); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
- if ($in_affix) {
+ if ($affix['class'] !== '') {
echo '
';
}
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
@@ -209,9 +212,7 @@ class RecentCommentsWidget extends \WP_Widget {
-
-
-
+ render_common_form_section($instance); ?>
*/
public function update($new_instance, $old_instance): array {
- return [
+ return $this->update_common_fields($new_instance, $old_instance, [
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
'count' => max(1, min(20, (int) ($new_instance['count'] ?? 5))),
'show_avatar' => !empty($new_instance['show_avatar']) ? 1 : 0,
'outpost' => sanitize_text_field((string) ($new_instance['outpost'] ?? '')),
'outer' => sanitize_text_field((string) ($new_instance['outer'] ?? '')),
- ];
+ ]);
}
}
diff --git a/src/AKSRT/Widgets/SearchWidget.php b/src/AKSRT/Widgets/SearchWidget.php
index 3120ce75..fd4fec35 100644
--- a/src/AKSRT/Widgets/SearchWidget.php
+++ b/src/AKSRT/Widgets/SearchWidget.php
@@ -7,9 +7,12 @@
namespace AKSRT\Widgets;
+use AKSRT\Widgets\Traits\CommonWidgetFields;
+
defined('ABSPATH') || exit;
class SearchWidget extends \WP_Widget {
+ use CommonWidgetFields;
public function __construct() {
parent::__construct('aksrt_search', esc_html__('AKSRT 搜索', 'aksrt'), [
@@ -32,10 +35,20 @@ class SearchWidget extends \WP_Widget {
$show_kw = !empty($instance['show_keywords']);
$show_history = !empty($instance['show_history']);
$show_types = !empty($instance['show_type']) || !isset($instance['show_type']);
+ // NOTE: SearchWidget owns a business `show_type` checkbox (search-type
+ // toggle) that shares the key name with the common `show_type` (device
+ // visibility). To avoid an admin-form collision, this widget does NOT
+ // render the shared common-form section (render_common_form_section is
+ // intentionally not called). The shared affix helper is still wired in
+ // widget() via aksrt_widget_affix_attrs(), and update_common_fields()
+ // persists common keys — but note the shared show_type cannot be set
+ // here, so display_callback treats it as 'all' (always visible).
$show_cat = !empty($instance['show_input_cat']);
$kw_title = sanitize_text_field((string) ($instance['keywords_title'] ?? __('热门搜索', 'aksrt')));
$more_but = sanitize_text_field((string) ($instance['more_but'] ?? ''));
$more_url = esc_url_raw(trim((string) ($instance['more_but_url'] ?? '')));
+ // Affix helper (shared layer sidebar_affix).
+ $affix = function_exists('aksrt_widget_affix_attrs') ? aksrt_widget_affix_attrs($instance) : ['class' => '', 'attr' => ''];
$state = function_exists('aksrt_get_search_form_state')
? aksrt_get_search_form_state()
@@ -109,7 +122,7 @@ class SearchWidget extends \WP_Widget {
$uid = 'aksrt-sw-' . esc_attr($this->id);
?>
-