From 5061977b7203e6765049bb6672a69c6c41e50cf7 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 30 Jul 2026 18:56:16 +0800 Subject: [PATCH 1/8] plan: zibll widgets 1:1 replication 6-phase vertical-slice plan for full 1:1 replication of the Zibll widget system into AKSRT. Covers pagination fatal fix, card-stacking CSS fix, unified renderer, shared common-form layer adoption, and per-category widget alignment (posts/users/slider/marketing/forum/shop) plus resource migration and registration/block-editor parity. Co-Authored-By: Claude Fable 5 --- .specs/plans/zibll-widgets-1to1.md | 148 +++++++++++++++++++++++++++++ .specs/prds/zibll-widgets-1to1.md | 41 ++++++++ 2 files changed, 189 insertions(+) create mode 100644 .specs/plans/zibll-widgets-1to1.md create mode 100644 .specs/prds/zibll-widgets-1to1.md diff --git a/.specs/plans/zibll-widgets-1to1.md b/.specs/plans/zibll-widgets-1to1.md new file mode 100644 index 00000000..f511f044 --- /dev/null +++ b/.specs/plans/zibll-widgets-1to1.md @@ -0,0 +1,148 @@ +# Plan: Zibll 小工具全量 1:1 深度复刻 + +> PRD: `.specs/prds/zibll-widgets-1to1.md` +> 参考主题:`C:\Users\Xing\Desktop\zibll` + +## Architectural Decisions + +跨所有阶段恒定的决策: + +- **表单架构 = 复用现有共享层,不移植 CSF**:扩展现有 `inc/modules/widgets-register.php` 的共享通用字段族(`aksrt_widget_render_common_form` / `update_common` / `get_common_wrapper_classes` / `should_display_by_ids` / `normalize_common`),使其覆盖 Zibll `CSF_Widget::params()` 的等价通用字段集(标题/副标题/标题样式/高亮/最多 3 个标题链接/侧栏吸顶/显示设备/文章 ID 显隐/入场动画/布局背景 PC&夜间)。所有小工具通过共享层,不再各自手写。**不引入 Codestar (CSF) 框架。** +- **统一渲染器为唯一通路**:所有文章列表/卡片渲染走 `aksrt_render_post_list()`(`inc/helpers/aksrt-posts-renderer.php`)。删除 `archive-resources.php` 等手写 `.posts-row.ajaxpager` + `get_template_part` 的重复路径。 +- **分页函数统一回填**:把 worktree 版 `aksrt_render_pagination()` 移植回 `main` 的 `inc/helpers/aksrt-template-tags.php`,作为唯一分页实现。 +- **小工具基座 = 单一 trait**:新增 `src/AKSRT/Widgets/Traits/CommonWidgetFields.php`(或等价),封装 form/update/render-common 的共享调用,消除 16 处 `in_affix` 重复实现。每个小工具 `use` 该 trait,自身只声明业务字段。 +- **注册 = PSR-4 + `inc/modules/widgets-register.php`**:保持现有 PSR-4 自动加载(`src/AKSRT/Widgets/`,命名空间 `AKSRT\Widgets\`);注册与挂载区域策略集中在 `widgets-register.php`。 +- **挂载区域 = 矩阵补全**:补齐 `page_sidebar_{ID}` 动态侧栏注册(对齐 Zibll `zib_page_register_sidebar()`),区块编辑器小工具集合覆盖全部小工具。 +- **资源搬运**:把 Zibll `img/`(slider-bg、slider-layer、user-level-*、medal/、null*.svg、thumbnail*.svg 等)及 CSF 后台选择器缩略图拷入 `assets/img/`(或 `assets/images/`),后续逐步替换。**不移植 CSF 框架本体。** +- **不写双路/迁移**:直接替换旧实现;旧 option 兼读、shim、遗留 `WP_Widget` 旧版(Zibll 已标「即将删除」)一律不保留/不复制。 +- **文件体量**:遵循 CLAUDE.md ≤700 行目标、1000 行硬限;超限即拆 trait / 子文件。 +- **文档对齐**:修正 CLAUDE.md 路径漂移(`inc/classes/widgets/` → `src/AKSRT/Widgets/`)。 + +--- + +## Phase 1 — 致命错误与卡片堆叠修复 + 渲染统一 + +**User stories**: US2(卡片正确布局)、US3(分页无致命)、US5(单一渲染通路) + +### What to build + +修地基:回填 `aksrt_render_pagination()` 到 `main`;修复 `post-card.css` 的 `display: block !important` 使卡片恢复 inline-block 网格;删除 `archive-resources.php` 手写卡片循环,改走 `aksrt_render_post_list()` 统一渲染器。完成后首页/归档/搜索页有文章时不再致命,卡片在 list/card/mini 三模式下正确布局、不堆叠。 + +### 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 行;超限已拆。 + +--- + +## Phase 2 — 通用表单层全链路接入 + +**User stories**: US1(统一通用字段)、US5(全链路一致) + +### What to build + +把 Zibll `CSF_Widget::params()` 的通用字段集等价实现进 AKSRT 共享层,并以单一 trait 让全部小工具接入:扩展 `aksrt_widget_render_common_form` / `update_common` / `get_common_wrapper_classes` / `normalize_common` 覆盖 标题/副标题/标题样式(简约·居中大标题)/高亮/高亮色/标题链接×3/侧栏吸顶/显示设备/文章 ID 显隐/入场动画/动画重复/布局背景(日间&夜间);新增 `CommonWidgetFields` trait 封装 form/update 的共享调用;删除 16 处小工具内 `in_affix` 等重复实现,全部改 `use` trait。 + +### 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 行。 + +--- + +## Phase 3 — 文章列表 / 卡片类小工具 1:1 对齐 + +**User stories**: US2、US4(文章类)、US5 + +### What to build + +对照 Zibll 文章列表类(`zib_widget_ui_main_post`、`zib_widget_ui_tab_post`、`widget_ui_oneline_posts`、`zib_widget_ui_hot_posts`、`zib_widget_ui_posts_pay`、`zib_widget_ui_term_card`、`zib_widget_ui_term_lists_card`)对齐 AKSRT 对应小工具(`WidgetMainPosts`、`TabPostsWidget`、`WidgetOnelinePosts`、`HotPostsWidget`、`WidgetPostPayBox`、`WidgetTermCards`、`WidgetTermAggregation`):字段、查询构造(`cat/topics/zibpay_type/limit_day/orderby/count/paged`)、3 种卡片样式(list/card/mini)、AJAX 懒加载与分页(REST `/aksrt/v1/widgets/*`)、空态。统一经 `aksrt_render_post_list()` 渲染。 + +### Done when + +- [ ] 上述 7 个小工具各自 `form()` 业务字段与 Zibll 对齐(对照 `C:\Users\Xing\Desktop\zibll\inc\widgets\widget-posts.php` 同名 `Zib_CFSwidget::create()` 的 `fields` 数组逐项核对,差异记录在阶段说明)。 +- [ ] `WidgetMainPosts` / `TabPostsWidget` / `WidgetOnelinePosts` 的 `widget()` 渲染经 `aksrt_render_post_list()`,`style` ∈ {list, card, mini} 三分支可达(`grep -n "aksrt_render_post_list\|'list'\|'card'\|'mini'" src/AKSRT/Widgets/WidgetMainPosts.php` 确认)。 +- [ ] `src/AKSRT/Core/WidgetAjax.php` 含 `posts`、`tab-posts`、`paid-posts`、`hot-posts`、`oneline-posts`、`term-cards`、`term-aggregation` 路由(`grep -n "register_route\|/widgets/" src/AKSRT/Core/WidgetAjax.php` 确认),均带 `permission_callback`。 +- [ ] `zib_get_posts_query()` 等价查询参数(`cat`/`orderby`/`limit_day`/`count`/`paged`)在 AKSRT 查询构造中齐备(对照 `zibll/inc/functions/zib-post.php:427 zib_get_posts_query`)。 +- [ ] 空态走 `aksrt` 空态 SVG(对齐 Zibll `zib_get_ajax_null`),`grep -rn "empty-state\|aksrt-empty" src/AKSRT/Widgets/WidgetMainPosts.php` 确认。 +- [ ] `php tools/audit-admin-contracts.php` 退出码 0;改动文件 ≤1000 行。 + +--- + +## Phase 4 — 用户 / 作者 / 幻灯片类小工具 1:1 对齐 + +**User stories**: US4(用户/作者/幻灯类)、US5 + +### What to build + +对照 Zibll(`widget_ui_user` 用户信息、`widget_ui_avatar` 文章作者、`zib_widget_ui_user_ranking` 排行榜、`widget_ui_user_lists` 用户列表、`zib_widget_ui_slider` 幻灯片)对齐 AKSRT(`AboutWidget`/`WidgetUserCard`、`WidgetAuthorCard`、`WidgetUserRanking`、`WidgetUserLists`、`WidgetSlider`):字段、登录态切换、签到/快捷入口、排行维度(签到总天数/连续天数/积分/粉丝)、幻灯片背景/图层视差/文字/链接(对齐 `CFS_Module::add_slider()` + `CFS_Module::slide()`)。 + +### Done when + +- [ ] 5 个小工具业务字段与 Zibll 对齐(对照 `zibll/inc/widgets/widget-user.php`、`widget-slider.php` 及 `inc/options/options-module.php` 的 `add_slider`/`slide`)。 +- [ ] `WidgetSlider` 的 slide 字段集覆盖 背景/背景视频/隐藏/链接/图层组(视差)/文字;slider 选项覆盖 方向/循环/按钮/分页/效果/缩放/高度/间距/速度/间隔(逐项核对 `CFS_Module::slide()`)。 +- [ ] 用户类小工具登录态分支:未登录→登录/注册卡(含 OAuth),已登录→用户信息卡(对齐 `widget_ui_user`)。 +- [ ] `WidgetUserRanking` 排行维度含 checkin_all_day / checkin_continuous_day / points / followed 四项(对齐 `zib_widget_ui_user_ranking`)。 +- [ ] 幻灯片前端走共享 Swiper 栈(`assets/js/core/slider.js` + `swiper-bundle`),`grep -n "swiper\|slider" src/AKSRT/Widgets/WidgetSlider.php` 确认。 +- [ ] `php tools/audit-admin-contracts.php` 退出码 0;改动文件 ≤1000 行。 + +--- + +## Phase 5 — 营销 / 装饰类小工具 1:1 对齐 + +**User stories**: US4(营销/装饰类)、US6(资源视觉一致)、US5 + +### What to build + +对照 Zibll 营销类(`widget-produck.php`:定价/评价/FAQ/统计/亮点/图文亮点/3D交互)与装饰类(`widget-more.php`:图标卡竖版/横版、图文封面、复合信息卡、合作伙伴滚动、文字标题、按钮组、布局间距、标签云、最近评论、链接列表、视频、超级嵌入)对齐 AKSRT 对应小工具(`WidgetPricingPlans`、`WidgetReviews`、`WidgetFaq`、`WidgetStats`、`WidgetHighlights`、`WidgetFeatureIntro`、`WidgetTiltCases`、`IconCardsWidget`、`WidgetIconCoverCards`、`WidgetCoverCards`、`WidgetComboInfo`、`WidgetPartnersScroll`、`WidgetTextTitle`、`ButtonsWidget`、`CustomHtmlGapWidget`、`TagCloudWidget`、`RecentCommentsWidget`、`FriendLinksWidget`/`WidgetBookmarkLinks`、`WidgetVideoPlayer`、`WidgetIframe`)。同时搬运 Zibll 资源(图片/图标/CSS/JS)入 `assets/img`。 + +### Done when + +- [ ] 上述 20 个小工具业务字段与 Zibll 对齐(对照 `zibll/inc/widgets/widget-produck.php`、`widget-more.php` 的 `fields` 数组逐项核对)。 +- [ ] `assets/img/` 含搬运资源:`slider-bg.jpg`、`slider-layer-1.png`、`slider-layer-2.png`、`user_t.jpg`、`null*.svg`、`thumbnail*.svg`、`user-level-1..10.png`、`medal/`、`avatar-default.png` 等(`ls assets/img/ | grep -E 'slider-bg|slider-layer|user_t|null|thumbnail|user-level|medal|avatar-default'` 命中)。 +- [ ] 装饰类滚动/轮播(合作伙伴、评价、图标卡)走共享 Swiper 栈;按钮组/文字标题/复合信息卡字段与 Zibll 对齐。 +- [ ] 链接类(`FriendLinksWidget`/`WidgetBookmarkLinks`)支持 card/bigcard/image/simple 四型(对齐 `aksrt_links_box` / Zibll `widget_ui_links_lists_2`)。 +- [ ] `CustomHtmlGapWidget` 双模式(html / gap,PC&移动高度)与 Zibll `zib_widget_ui_layout_gap` 对齐。 +- [ ] `php tools/audit-admin-contracts.php` 退出码 0;改动文件 ≤1000 行。 + +--- + +## Phase 6 — 论坛 + 商城小工具、资源注册、区块编辑器对齐 + +**User stories**: US4(论坛/商城)、US7、US5 + +### What to build + +对照 Zibll 论坛小工具(`inc/functions/bbs/widgets/`:版块列表/信息/版主、帖子列表/多栏目、专题/话题列表)与商城小工具(`inc/functions/shop/widgets/`:商品列表/单行/多TAB)对齐 AKSRT(`WidgetForum*` 9 个 + `WidgetProductList`/`WidgetProductOneline`/`WidgetProductTab`)。补齐 `page_sidebar_{ID}` 动态侧栏注册;区块编辑器小工具集合覆盖全部小工具;修正 CLAUDE.md 路径漂移。 + +### Done when + +- [ ] 论坛 9 个小工具业务字段与 Zibll 对齐(对照 `zibll/inc/functions/bbs/widgets/widgets-plate.php`、`widgets-posts.php`、`widgets-term.php`)。 +- [ ] 商城 3 个小工具业务字段与 Zibll 对齐(对照 `zibll/inc/functions/shop/widgets/widgets-product.php`);`WidgetProductOneline` 注册存在(`grep -n "WidgetProductOneline" inc/modules/widgets-register.php` 命中)。 +- [ ] `WidgetAjax.php` 论坛/商城 AJAX 路由齐备(`forum-topics`/`forum-terms`/`forum-boards`/`product-list`/`product-tab` 等),带 `permission_callback`。 +- [ ] `page_sidebar_{ID}` 动态侧栏已注册(对齐 Zibll `zib_page_register_sidebar()`),`is_active_sidebar('page_sidebar_')` 对启用挂载的页面返回 true(一次性 PHP 断言脚本验证,置于 `$CLAUDE_JOB_DIR/tmp`)。 +- [ ] 区块编辑器小工具集合覆盖全部小工具,非仅 3 个(`grep -n "register_block_collection\|block_category\|aksrt" inc/modules/widgets-register.php` 核对登记范围)。 +- [ ] CLAUDE.md 小工具路径修正为 `src/AKSRT/Widgets/`(`grep -n "inc/classes/widgets" CLAUDE.md` 无残留或已注明实际路径)。 +- [ ] `php tools/audit-admin-contracts.php` 退出码 0;全部改动文件 ≤1000 行。 + +--- + +## Out of Scope + +- 不新增任何向后兼容 / 数据迁移层(PRD 明确)。 +- 参考图片为临时搬运,后续替换不属于本计划。 +- 不移植 CSF 框架本体。 + +## Open Questions + +- Zibll 9 个旧版 `WP_Widget`(标记「即将删除」)是否需要在 AKSRT 复刻旧版?默认**不复刻**(符合「不保留旧实现」),仅以现代对等小工具覆盖。若需保留旧版行为,请在 Phase 3/5 前确认。 +- `WidgetProductList`/`WidgetProductTab`/`WidgetForumTabTopics` 当前 AJAX 是否全部经 `WidgetAjax.php`?Phase 6 将统一收敛到 REST 路由;若某些小工具刻意只做客户端渲染,请确认。 +- 小工具挂载区域策略(`aksrt_widget_allowed_sidebars`)当前仅约束 7 个小工具,其余不受限。是否需要对照 Zibll 全量收紧?默认**保持现有宽松策略**,仅修复明确错误。 diff --git a/.specs/prds/zibll-widgets-1to1.md b/.specs/prds/zibll-widgets-1to1.md new file mode 100644 index 00000000..5230058c --- /dev/null +++ b/.specs/prds/zibll-widgets-1to1.md @@ -0,0 +1,41 @@ +# PRD: Zibll 小工具全量 1:1 深度复刻 + +> 来源:用户指令(/plan 参数)。参考主题:`C:\Users\Xing\Desktop\zibll` + +## 背景与问题 + +AKSRT 主题的小工具系统当前存在多处缺陷: + +1. **卡片堆叠问题**:`assets/css/frontend/post-card.css` 中 `.posts-item { display: block !important }`(行 32)覆盖了 `.posts-item.card { display: inline-block }`(行 75),导致卡片全部纵向堆叠、宽度 calc 失效。 +2. **分页函数缺失**:`aksrt_render_pagination()` 在 `main` 分支未定义,仅存在于 worktree 分支;`index.php:40`、`archive-resources.php:173`、`search.php:281` 三处无 `function_exists` 守卫直接调用,归档/搜索/首页有文章时即致命错误。 +3. **重复渲染路径**:`archive-resources.php:162-171` 手写 `.posts-row.ajaxpager` + `get_template_part('content-post-card')`,绕过 `aksrt_render_post_list()` 统一渲染器(违反 CLAUDE.md 行 147)。 +4. **通用表单链路半成品(双路)**:`inc/modules/widgets-register.php` 已有一套共享通用字段层(`aksrt_widget_render_common_form` / `update_common` / `get_common_wrapper_classes` / `should_display_by_ids`,行 314-509),但 **55 个小工具无一调用**;16 个小工具各自重新实现 `in_affix`,等于半成品双路。 +5. **小工具缺失/不对齐**:需对照 Zibll 参考主题(约 44 个现代 CSF 小工具 + 9 个旧版)逐一 1:1 对齐字段、渲染、AJAX 链路。 +6. **注册/区块编辑器/侧栏缺口**:`page_sidebar_{ID}` 动态侧栏未被注册(页面中段侧栏空白);区块编辑器仅收录 3/55 个小工具;CLAUDE.md 路径漂移(`inc/classes/widgets/` 实际为 `src/AKSRT/Widgets/`)。 + +## 目标 + +按照 Zibll 参考主题**全量 1:1 深度复刻**小工具系统,完成「设置 → 注册 → 后台表单 → 渲染过滤 → 前端渲染」全链路,消除卡片堆叠与致命错误,统一为单一路径。 + +## 约束 + +- **不写双路与迁移兼容代码**:直接替换旧实现,不保留旧 option 兼读、不新增 shim(遵循 CLAUDE.md「重写优先于兼容」)。 +- **可搬运参考主题资源**:允许把 Zibll 的图片/图标等资源搬入 AKSRT(后续逐步替换)。 +- 表单系统走「复用现有共享层」路线,**不移植 CSF (Codestar) 框架**。 +- 单一计划、分阶段推进;每阶段可独立演示与验证。 + +## 用户故事 + +- **US1(管理员)**:每个小工具都能通过统一的共享表单层配置 Zibll 等价的通用字段(显示设备 PC/移动、文章 ID 显隐、侧栏吸顶、入场动画、布局背景、标题/副标题/链接),不再有逐小工具重复实现。 +- **US2(访客)**:文章卡片在 list/card/mini 三种模式下正确布局,不堆叠、不塌陷。 +- **US3(访客)**:首页/归档/搜索页分页正常(数字 / 加载更多 / 无限滚动),无致命错误。 +- **US4(管理员)**:Zibll 的每一个小工具在 AKSRT 都有对等实现,可挂载到正确的挂载区域。 +- **US5(开发者)**:全链路(设置 → 注册 → 表单 → 渲染过滤 → 前端渲染)一致且单一通路。 +- **US6(开发者)**:参考主题的图片/图标/CSS/JS 资源已入库,小工具渲染视觉与 Zibll 一致(后续替换)。 +- **US7(访客/管理员)**:论坛版块/话题与商城商品小工具与 Zibll 对齐。 + +## 范围外 + +- 不新增任何向后兼容 / 数据迁移层(PRD 明确)。 +- 参考图片为临时搬运,后续替换不属于本计划。 +- 不移植 CSF 框架本体。 From 98aeee3cc18e8b30c54787ca0151f4ae4cc366e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 19:05:43 +0800 Subject: [PATCH 2/8] fix(phase1): backfill pagination, fix card stacking, unify archive render - Backfill aksrt_render_pagination() into inc/helpers/aksrt-template-tags.php (numbered + load_more/infinite branches, \AKSRT\Settings\Settings facade) - post-card.css: drop display:block !important from base .posts-item so .posts-item.card inline-block 3-col grid wins; scope block forcing - archive-resources.php: replace hand-written .posts-row.ajaxpager + get_template_part loop with aksrt_render_post_list() + aksrt_render_pagination() Refs .specs/plans/zibll-widgets-1to1.md Phase 1 (US2/US3/US5) Co-Authored-By: Claude Fable 5 --- archive-resources.php | 28 ++++--- assets/css/frontend/post-card.css | 8 +- inc/helpers/aksrt-template-tags.php | 116 ++++++++++++++++++++++++++++ 3 files changed, 139 insertions(+), 13 deletions(-) diff --git a/archive-resources.php b/archive-resources.php index 0edc2f88..facd0a68 100644 --- a/archive-resources.php +++ b/archive-resources.php @@ -159,18 +159,24 @@ $has_filter = $current_orderby !== 'date' || $current_price !== 'all' || $price_ -
- -
+ 'archive', + 'overrides' => ['mode' => 'card'], + ]) + : ['context' => 'archive', 'mode' => 'card']; - + if (function_exists('aksrt_render_post_list')) { + aksrt_render_post_list(null, array_merge($render_config, ['wrapper_class' => 'content-stream__list'])); + } + if (function_exists('aksrt_render_pagination')) { + aksrt_render_pagination(null, [ + 'query_context' => 'archive', + 'render_config' => $render_config, + ]); + } + ?>

diff --git a/assets/css/frontend/post-card.css b/assets/css/frontend/post-card.css index fa7ab527..fee34016 100644 --- a/assets/css/frontend/post-card.css +++ b/assets/css/frontend/post-card.css @@ -28,8 +28,6 @@ posts.posts-item, article.posts-item, .posts-item { - /* Custom element is inline by default — force block layout. */ - display: block !important; opacity: 1 !important; visibility: visible !important; padding: 20px; @@ -45,6 +43,12 @@ article.posts-item, opacity: 1; } +/* Custom element is inline by default — force block layout here only, + scoped so it never overrides .posts-item.card inline-block grid (Phase 1 fix). */ +posts.posts-item { + display: block; +} + .posts-item.list { display: block; width: 100%; diff --git a/inc/helpers/aksrt-template-tags.php b/inc/helpers/aksrt-template-tags.php index 307529e0..877ba5d6 100644 --- a/inc/helpers/aksrt-template-tags.php +++ b/inc/helpers/aksrt-template-tags.php @@ -1126,6 +1126,122 @@ if (!function_exists('aksrt_render_comment_pagination')) { } } +// ============================================================ +// 模板标签:文章列表分页渲染 +// ============================================================ + +if (!function_exists('aksrt_render_pagination')) { + /** + * 渲染文章列表分页(根据后台配置选择模式)。 + * + * Zibll 对齐:numbered = 数字分页按钮;load_more/infinite = AJAX 追加。 + * + * @param WP_Query|null $query 可选,传入自定义查询对象;默认用主查询。 + * @param array $args { + * @type string $query_context search|archive|home|tag|category... 供 AJAX 路由解析。 + * @type array $render_config 透传给 AJAX 渲染器(aksrt_resolve_post_list_render_config)。 + * } + */ + function aksrt_render_pagination(?WP_Query $query = null, array $args = []): void { + $pagination_mode = class_exists('\AKSRT\Settings\Settings') + ? \AKSRT\Settings\Settings::get_post_pagination_mode() + : 'numbered'; + + if ($pagination_mode === 'numbered') { + global $wp_query; + $q = $query ?? $wp_query; + if (!($q instanceof WP_Query) || $q->max_num_pages <= 1) { + return; + } + + $current_page = max(1, (int) $q->get('paged'), (int) get_query_var('paged'), (int) get_query_var('page')); + $max_pages = (int) $q->max_num_pages; + $links = paginate_links([ + 'total' => $max_pages, + 'current' => $current_page, + 'mid_size' => 2, + 'prev_text' => '', + 'next_text' => '', + 'type' => 'plain', + ]); + + if (empty($links)) { + return; + } + + echo ''; + return; + } + + // load_more / infinite 模式 + global $wp_query; + $q = $query ?? $wp_query; + if (!($q instanceof WP_Query)) { + return; + } + $max_pages = (int) $q->max_num_pages; + $current_page = max(1, (int) $q->get('paged'), (int) get_query_var('paged'), (int) get_query_var('page')); + + if ($current_page >= $max_pages) { + return; + } + + $query_context = ''; + if (!empty($args['query_context']) && is_string($args['query_context'])) { + $query_context = sanitize_key($args['query_context']); + } elseif (is_search()) { + $query_context = 'search'; + } elseif (is_archive()) { + $query_context = 'archive'; + } + + $query_vars = $q->query_vars; + if (!is_array($query_vars)) { + $query_vars = []; + } else { + unset($query_vars['paged'], $query_vars['page']); + } + + $render_config = !empty($args['render_config']) && is_array($args['render_config']) + ? $args['render_config'] + : []; + + $trigger_text = __('加载更多', 'aksrt'); + if (class_exists('\AKSRT\Settings\Settings')) { + $list_settings = \AKSRT\Settings\Settings::get_post_list_style_settings(); + if (!empty($list_settings['ajax_trigger_text'])) { + $trigger_text = (string) $list_settings['ajax_trigger_text']; + } + } + ?> + + Date: Thu, 30 Jul 2026 20:42:46 +0800 Subject: [PATCH 3/8] 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 = '
' . wp_kses_post($text) . '
'; + } + 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); ?> -