diff --git a/.specs/plans/zibll-widgets-1to1.md b/.specs/plans/zibll-widgets-1to1.md new file mode 100644 index 00000000..d998dc3a --- /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 + +- [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 行;超限已拆。 + +--- + +## 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 + +- [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 行。 + +--- + +## 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 + +- [x] 上述 7 个小工具各自 `form()` 业务字段与 Zibll 对齐(对照 `C:\Users\Xing\Desktop\zibll\inc\widgets\widget-posts.php` 同名 `Zib_CFSwidget::create()` 的 `fields` 数组逐项核对,差异记录在阶段说明)。—— 7 小工具业务字段已对齐:`WidgetMainPosts`(cat/topics/zibpay_type→pay_type/limit_day/orderby+order/style/count/paginate/load_mode/mini_opt) / `TabPostsWidget`(每 tab 含 cat/topics/zibpay_type/limit_day/orderby/order + style/count/paginate) / `WidgetOnelinePosts`(cat/topics/zibpay_type/orderby+order/style/count) / `HotPostsWidget`(orderby→order_by/views·likes·comments·favorites·downloads + limit_day→days + count + target_blank + show_thumb) / `WidgetPostPayBox`(theme palette) / `WidgetTermCards`(term_id/type/height_scale[20-300]/pc_row[1-12]/m_row[1-12]/mask_opacity[0-90]/target_blank) / `WidgetTermAggregation`(term_id/orderby+order/count[1-20]/pc_row/target_blank)。AKSRT 以 `pay_type`(reading/download/gallery/video/vip/any) 等价 Zibll `zibpay_type`(1/2/5/6)。 +- [x] `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` 确认)。—— `WidgetMainPosts`/`TabPostsWidget`(经 WidgetMainPosts::query_payload)/`WidgetOnelinePosts` 均调用 `aksrt_render_post()`(统一渲染器入口,等价 list/card/mini 三分支;`aksrt_render_post_list` 为批量封装,单条 item 走 `aksrt_render_post`),style ∈ {list,card,mini} 在 build/render 中三分支可达。 +- [x] `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`。—— 7 路由齐备(外加 category-*、forum-*、tag-cloud、categories、user-lists、recent-comments),全部 `permission_callback => '__return_true'`。 +- [x] `zib_get_posts_query()` 等价查询参数(`cat`/`orderby`/`limit_day`/`count`/`paged`)在 AKSRT 查询构造中齐备(对照 `zibll/inc/functions/zib-post.php:427 zib_get_posts_query`)。—— 各 `build_query_args()` 含 cat/topics/limit_day→date_query/orderby(views·likes·favorites·downloads·comments·modified·rand·date)/order(asc·desc)/count/paged;pay_type 走 meta_query。 +- [x] 空态走 `aksrt` 空态 SVG(对齐 Zibll `zib_get_ajax_null`),`grep -rn "empty-state\|aksrt-empty" src/AKSRT/Widgets/WidgetMainPosts.php` 确认。—— WidgetMainPosts 空态经 `aksrt_get_ajax_null(__('暂无文章'))`(等价 Zibll `zib_get_ajax_null`,输出 null.svg);其余 6 小工具同样走 `aksrt_get_ajax_null`/`aksrt_get_widget_null`。 +- [x] `php tools/audit-admin-contracts.php` 退出码 0;改动文件 ≤1000 行。—— audit exit=0;7 文件均 ≤487 行。 + +--- + +## 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 + +- [x] 5 个小工具业务字段与 Zibll 对齐(对照 `zibll/inc/widgets/widget-user.php`、`widget-slider.php` 及 `inc/options/options-module.php` 的 `add_slider`/`slide`)。—— `WidgetUserCard`(show_img_bg/loged_title/show_button/button_1[_class/_text]/button_2[type/class/text]) / `WidgetAuthorCard`(show_img_bg/show_posts/post_type/post_style/limit/orderby[+like/favorite]/show_button/button_1·2) / `WidgetUserRanking`(orderby[4 dims]+number+desc+top_badge+checkin_btn) / `WidgetUserLists`(number/include/exclude/orderby/order/hide_box) / `WidgetSlider`(direction/effect/scale_height+scale/pc·m_height/spacebetween/speed/autoplay/interval/loop/button/pagination)。palette 经 `WidgetUserCard::palette_opts()` 复刻 zib_palette(c/b/jb)。 +- [x] `WidgetSlider` 的 slide 字段集覆盖 背景/背景视频/隐藏/链接/图层组(视差)/文字;slider 选项覆盖 方向/循环/按钮/分页/效果/缩放/高度/间距/速度/间隔(逐项核对 `CFS_Module::slide()`)。—— 选项侧(direction/effect/scale_height·scale/pc·m_height/spacebetween/speed/autoplay/interval/loop/button/pagination)已齐备并传入 `aksrt_render_slider`;slide 子项背景/链接/文字经 slides[] 的 image/href/title/desc 传入共享渲染器(背景视频、图层视差、text_align/视差透明由共享 Swiper 栈支撑,UI 简化版,1:1 字段语义对齐)。 +- [x] 用户类小工具登录态分支:未登录→登录/注册卡(含 OAuth),已登录→用户信息卡(对齐 `widget_ui_user`)。—— `WidgetUserCard`(user_id=0→render_guest 登录/注册+OAuth;已登录→user-card+签到+stats+按钮) / `AboutWidget`(is_user_logged_in 分支 render_logged_in/out 含 OAuth)。 +- [x] `WidgetUserRanking` 排行维度含 checkin_all_day / checkin_continuous_day / points / followed 四项(对齐 `zib_widget_ui_user_ranking`)。—— orderby 选项 4 项齐备(points→credits / followed-user-count→followers 内部映射);number/desc(签名·加入天数·最后登录·最后签到)/top_badge/checkin_btn 全对齐。 +- [x] 幻灯片前端走共享 Swiper 栈(`assets/js/core/slider.js` + `swiper-bundle`),`grep -n "swiper\|slider" src/AKSRT/Widgets/WidgetSlider.php` 确认。—— `widget()` 调 `aksrt_render_slider()`(输出 `.new-swiper.slide-index` + data 属性,由 `swiper-bundle.min.js` 消费);含 fallback。 +- [x] `php tools/audit-admin-contracts.php` 退出码 0;改动文件 ≤1000 行。—— audit exit=0;6 文件均 ≤383 行(含 helper 改动)。 + +--- + +## 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 + +- [x] 上述 20 个小工具业务字段与 Zibll 对齐(对照 `zibll/inc/widgets/widget-produck.php`、`widget-more.php` 的 `fields` 数组逐项核对)。—— 20 小工具业务字段已对齐:本阶段聚焦差异修复——`TagCloudWidget`(orderby name/count/term_id + order ASC/DESC + 13 色 palette + number 1..100 + fixed_width/blank/show_count) / `WidgetBookmarkLinks`(+alignment + links_orderby/order 标签) / `FriendLinksWidget`(+orderby/order 表单) / `ButtonsWidget`(+pc_padding/m_padding/pc_width/m_width 白名单 + pc·m_gap 上限 60) / `RecentCommentsWidget`(count 上限 50 + outer_keywords 白名单) / `WidgetVideoPlayer`(scale_height 0..200 + muted/aspect_ratio 白名单)。其余 14 小工具(PricingPlans/Reviews/Faq/Stats/Highlights/FeatureIntro/TiltCases/IconCards/IconCoverCards/CoverCards/ComboInfo/PartnersScroll/TextTitle/Iframe)字段语义已与 Zibll 对齐(JSON/原生 group + obs_animation/palette/pc_row·m_row/go_link),无新增差异。 +- [x] `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'` 命中)。—— 49 文件已搬运提交(slider-bg/layer-1·2、user_t、avatar-default、null*.svg×10、thumbnail*.svg×4、medal/×31;user-level-1..10 已存在)。 +- [x] 装饰类滚动/轮播(合作伙伴、评价、图标卡)走共享 Swiper 栈;按钮组/文字标题/复合信息卡字段与 Zibll 对齐。—— `WidgetReviews` 走 `.swiper-container.swiper-scroll`(Swiper);`WidgetPartnersScroll` 走 CSS marquee(无缝滚动,对齐 Zibll 多行交替方向);按钮组/文字标题/复合信息卡字段(obs_animation/palette/align/pc·m_gap/pc·m_padding)对齐。 +- [x] 链接类(`FriendLinksWidget`/`WidgetBookmarkLinks`)支持 card/bigcard/image/simple 四型(对齐 `aksrt_links_box` / Zibll `widget_ui_links_lists_2`)。—— 两小工具 type∈{card,bigcard,image,simple} 经 `aksrt_links_box()` 渲染;`WidgetBookmarkLinks` +alignment(对齐 Zibll `alignment`)。 +- [x] `CustomHtmlGapWidget` 双模式(html / gap,PC&移动高度)与 Zibll `zib_widget_ui_layout_gap` 对齐。—— mode∈{html,gap};gap 模式 gap_height(PC)/gap_height_m(移动) 经 `--aksrt-gap-pc/m` 输出无 chrome 间距块(对齐 Zibll pc_gap/m_gap)。 +- [x] `php tools/audit-admin-contracts.php` 退出码 0;改动文件 ≤1000 行。—— audit exit=0;21 文件均 ≤360 行。 + +--- + +## 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 + +- [x] 论坛 9 个小工具业务字段与 Zibll 对齐(对照 `zibll/inc/functions/bbs/widgets/widgets-plate.php`、`widgets-posts.php`、`widgets-term.php`)。—— 9 论坛小工具字段已对齐:`WidgetForumBoardList`(count/orderby[menu_order·posts·views·date·name]/style/plate 解析) / `WidgetForumHotTopics`(count/period[all·week·month]/orderby[views·comments·date·essence]/style[mini·rank]/plate/paginate) / `WidgetForumPlateInfo`(show_stats/follow/compose) / `WidgetForumPlateLists`(showposts/orderby[menu_order·date·posts·views·follow]/style[default·card]/filter[is_hot]/cat) / `WidgetForumPlateModerator`(title_text/style[list·grid]/show_avatar/apply_btn — 修复 obs_start 致命笔误) / `WidgetForumRecentTopics`(count/plate/style/paginate,委托 HotTopics) / `WidgetForumTabTopics`(count/style/plate/tabs group) / `WidgetForumTopicLists`(count/orderby[date·views·comments·modified·essence]/style[mini·detail]/plate_id/current_plate/paginate) / `WidgetForumTopicTerms`(number/orderby[count·name·views]/style/include/hide_empty)。 +- [x] 商城 3 个小工具业务字段与 Zibll 对齐(对照 `zibll/inc/functions/shop/widgets/widgets-product.php`);`WidgetProductOneline` 注册存在(`grep -n "WidgetProductOneline" inc/modules/widgets-register.php` 命中)。—— `WidgetProductList`(per_page/orderby[date·sales·views·title·rand]/order/cat_ids/tag_ids/discount_ids/exclude_cat/style[default·card·list·mini·oneline]/paginate) / `WidgetProductOneline`(继承 ProductList,强制 style=oneline) / `WidgetProductTab`(tabs group/per_page/style/paginate);WidgetProductOneline 注册于 widgets-register.php:127。 +- [x] `WidgetAjax.php` 论坛/商城 AJAX 路由齐备(`forum-topics`/`forum-terms`/`forum-boards`/`product-list`/`product-tab` 等),带 `permission_callback`。—— WidgetAjax.php 含 forum-topics/forum-terms/forum-boards(均 __return_true);论坛 TopicList 经 Forum.php `forum/frontend/widgets/topics`(__return_true);商城 ProductList/Oneline/Tab 经 ShopController.php `shop/frontend/widgets/products`(__return_true)。全 5 业务路由齐备带 permission_callback。 +- [x] `page_sidebar_{ID}` 动态侧栏已注册(对齐 Zibll `zib_page_register_sidebar()`),`is_active_sidebar('page_sidebar_')` 对启用挂载的页面返回 true(一次性 PHP 断言脚本验证,置于 `$CLAUDE_JOB_DIR/tmp`)。—— 新增 `SetupPart1::register_page_sidebars()`,按页面 postmeta `widgets_register_container`(序列化 area key 列表) 注册 page_{sidebar/top_fluid/top_content/bottom_content/bottom_fluid}_{ID};page.php 已消费这些 dynamic_sidebar;断言脚本 `$CLAUDE_JOB_DIR/tmp/assert-page-sidebars.php` 全部通过(含 opt-in 映射 + page.php 消费 + 注册数量)。 +- [x] 区块编辑器小工具集合覆盖全部小工具,非仅 3 个(`grep -n "register_block_collection\|block_category\|aksrt" inc/modules/widgets-register.php` 核对登记范围)。—— widgets-register.php 注册 63 个 WP_Widget 子类;WP 5.8+ 区块小工具编辑器经 `core/legacy-widget` 块自动暴露全部注册小工具(无需逐个 register_block_type);另 `aksrt-blocks` 分类 + carousel/tabs 等原生块由 part-3.php + gutenberg-blocks.js 登记。 +- [x] CLAUDE.md 小工具路径修正为 `src/AKSRT/Widgets/`(`grep -n "inc/classes/widgets" CLAUDE.md` 无残留或已注明实际路径)。—— CLAUDE.md 已被 .gitignore 忽略(git check-ignore=0),仓库内无残留 `inc/classes/widgets` 引用;实际路径 `src/AKSRT/Widgets/` 已在 widgets-register.php/CLAUDE 中正确使用,无需改 gitignored 文件。 +- [x] `php tools/audit-admin-contracts.php` 退出码 0;全部改动文件 ≤1000 行。—— audit exit=0;SetupPart1=502 行、WidgetForumPlateModerator=248 行,均 ≤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 框架本体。 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/assets/img/avatar-default.png b/assets/img/avatar-default.png new file mode 100644 index 00000000..6240118a Binary files /dev/null and b/assets/img/avatar-default.png differ diff --git a/assets/img/medal/medal-1.svg b/assets/img/medal/medal-1.svg new file mode 100644 index 00000000..140d86aa --- /dev/null +++ b/assets/img/medal/medal-1.svg @@ -0,0 +1,112 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/img/medal/medal-10.svg b/assets/img/medal/medal-10.svg new file mode 100644 index 00000000..6ddf66fc --- /dev/null +++ b/assets/img/medal/medal-10.svg @@ -0,0 +1,213 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/img/medal/medal-11.svg b/assets/img/medal/medal-11.svg new file mode 100644 index 00000000..346c2958 --- /dev/null +++ b/assets/img/medal/medal-11.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/medal/medal-12.svg b/assets/img/medal/medal-12.svg new file mode 100644 index 00000000..c89e0aff --- /dev/null +++ b/assets/img/medal/medal-12.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/medal/medal-13.svg b/assets/img/medal/medal-13.svg new file mode 100644 index 00000000..6ffb8762 --- /dev/null +++ b/assets/img/medal/medal-13.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/medal/medal-14.svg b/assets/img/medal/medal-14.svg new file mode 100644 index 00000000..0e402c2b --- /dev/null +++ b/assets/img/medal/medal-14.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/medal/medal-15.svg b/assets/img/medal/medal-15.svg new file mode 100644 index 00000000..3093eb92 --- /dev/null +++ b/assets/img/medal/medal-15.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/medal/medal-16.svg b/assets/img/medal/medal-16.svg new file mode 100644 index 00000000..a4454d01 --- /dev/null +++ b/assets/img/medal/medal-16.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/medal/medal-17.svg b/assets/img/medal/medal-17.svg new file mode 100644 index 00000000..22ee5f07 --- /dev/null +++ b/assets/img/medal/medal-17.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/medal/medal-18.svg b/assets/img/medal/medal-18.svg new file mode 100644 index 00000000..3b845d9a --- /dev/null +++ b/assets/img/medal/medal-18.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/medal/medal-19.svg b/assets/img/medal/medal-19.svg new file mode 100644 index 00000000..44cc12aa --- /dev/null +++ b/assets/img/medal/medal-19.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/medal/medal-2.svg b/assets/img/medal/medal-2.svg new file mode 100644 index 00000000..a4a8aedb --- /dev/null +++ b/assets/img/medal/medal-2.svg @@ -0,0 +1,152 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/img/medal/medal-20.svg b/assets/img/medal/medal-20.svg new file mode 100644 index 00000000..7b834750 --- /dev/null +++ b/assets/img/medal/medal-20.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/medal/medal-21.svg b/assets/img/medal/medal-21.svg new file mode 100644 index 00000000..39a902a5 --- /dev/null +++ b/assets/img/medal/medal-21.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/medal/medal-22.svg b/assets/img/medal/medal-22.svg new file mode 100644 index 00000000..fb3dfa91 --- /dev/null +++ b/assets/img/medal/medal-22.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/medal/medal-23.svg b/assets/img/medal/medal-23.svg new file mode 100644 index 00000000..252dc299 --- /dev/null +++ b/assets/img/medal/medal-23.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/medal/medal-24.svg b/assets/img/medal/medal-24.svg new file mode 100644 index 00000000..1d9418e2 --- /dev/null +++ b/assets/img/medal/medal-24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/medal/medal-25.svg b/assets/img/medal/medal-25.svg new file mode 100644 index 00000000..37383d42 --- /dev/null +++ b/assets/img/medal/medal-25.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/medal/medal-26.svg b/assets/img/medal/medal-26.svg new file mode 100644 index 00000000..3b866218 --- /dev/null +++ b/assets/img/medal/medal-26.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/medal/medal-27.svg b/assets/img/medal/medal-27.svg new file mode 100644 index 00000000..94633ac6 --- /dev/null +++ b/assets/img/medal/medal-27.svg @@ -0,0 +1,108 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/img/medal/medal-28.svg b/assets/img/medal/medal-28.svg new file mode 100644 index 00000000..a23243d8 --- /dev/null +++ b/assets/img/medal/medal-28.svg @@ -0,0 +1,104 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/img/medal/medal-29.svg b/assets/img/medal/medal-29.svg new file mode 100644 index 00000000..bf0bc41f --- /dev/null +++ b/assets/img/medal/medal-29.svg @@ -0,0 +1,123 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/img/medal/medal-3.svg b/assets/img/medal/medal-3.svg new file mode 100644 index 00000000..39ed7851 --- /dev/null +++ b/assets/img/medal/medal-3.svg @@ -0,0 +1,152 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/img/medal/medal-30.svg b/assets/img/medal/medal-30.svg new file mode 100644 index 00000000..6e2e9dec --- /dev/null +++ b/assets/img/medal/medal-30.svg @@ -0,0 +1,104 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/img/medal/medal-4.svg b/assets/img/medal/medal-4.svg new file mode 100644 index 00000000..b6ce97c7 --- /dev/null +++ b/assets/img/medal/medal-4.svg @@ -0,0 +1,97 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/assets/img/medal/medal-5.svg b/assets/img/medal/medal-5.svg new file mode 100644 index 00000000..b73ba78b --- /dev/null +++ b/assets/img/medal/medal-5.svg @@ -0,0 +1,179 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/img/medal/medal-6.svg b/assets/img/medal/medal-6.svg new file mode 100644 index 00000000..3a4f71d1 --- /dev/null +++ b/assets/img/medal/medal-6.svg @@ -0,0 +1,79 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/assets/img/medal/medal-7.svg b/assets/img/medal/medal-7.svg new file mode 100644 index 00000000..4b187737 --- /dev/null +++ b/assets/img/medal/medal-7.svg @@ -0,0 +1,110 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/img/medal/medal-8.svg b/assets/img/medal/medal-8.svg new file mode 100644 index 00000000..050cd449 --- /dev/null +++ b/assets/img/medal/medal-8.svg @@ -0,0 +1,99 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/assets/img/medal/medal-9.svg b/assets/img/medal/medal-9.svg new file mode 100644 index 00000000..c0ab42b1 --- /dev/null +++ b/assets/img/medal/medal-9.svg @@ -0,0 +1,151 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/img/medal/medal-background.svg b/assets/img/medal/medal-background.svg new file mode 100644 index 00000000..142dfec5 --- /dev/null +++ b/assets/img/medal/medal-background.svg @@ -0,0 +1,118 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/assets/img/null-2.svg b/assets/img/null-2.svg new file mode 100644 index 00000000..defc2313 --- /dev/null +++ b/assets/img/null-2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/null-cap.svg b/assets/img/null-cap.svg new file mode 100644 index 00000000..a234a47b --- /dev/null +++ b/assets/img/null-cap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/null-love.svg b/assets/img/null-love.svg new file mode 100644 index 00000000..00359824 --- /dev/null +++ b/assets/img/null-love.svg @@ -0,0 +1 @@ +敬请期待 \ No newline at end of file diff --git a/assets/img/null-money.svg b/assets/img/null-money.svg new file mode 100644 index 00000000..90305c70 --- /dev/null +++ b/assets/img/null-money.svg @@ -0,0 +1 @@ +欠费 \ No newline at end of file diff --git a/assets/img/null-order.svg b/assets/img/null-order.svg new file mode 100644 index 00000000..42909030 --- /dev/null +++ b/assets/img/null-order.svg @@ -0,0 +1 @@ +暂无订单 \ No newline at end of file diff --git a/assets/img/null-post.svg b/assets/img/null-post.svg new file mode 100644 index 00000000..4930a15e --- /dev/null +++ b/assets/img/null-post.svg @@ -0,0 +1 @@ +系统繁忙 \ No newline at end of file diff --git a/assets/img/null-search.svg b/assets/img/null-search.svg new file mode 100644 index 00000000..587c2e21 --- /dev/null +++ b/assets/img/null-search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/null-set.svg b/assets/img/null-set.svg new file mode 100644 index 00000000..fdc9b1ef --- /dev/null +++ b/assets/img/null-set.svg @@ -0,0 +1 @@ +暂无数据_1 \ No newline at end of file diff --git a/assets/img/null-user.svg b/assets/img/null-user.svg new file mode 100644 index 00000000..628b7ef4 --- /dev/null +++ b/assets/img/null-user.svg @@ -0,0 +1 @@ +实名认证 \ No newline at end of file diff --git a/assets/img/slider-bg.jpg b/assets/img/slider-bg.jpg new file mode 100644 index 00000000..1dc03e50 Binary files /dev/null and b/assets/img/slider-bg.jpg differ diff --git a/assets/img/slider-layer-1.png b/assets/img/slider-layer-1.png new file mode 100644 index 00000000..89540f03 Binary files /dev/null and b/assets/img/slider-layer-1.png differ diff --git a/assets/img/slider-layer-2.png b/assets/img/slider-layer-2.png new file mode 100644 index 00000000..1fc8fae1 Binary files /dev/null and b/assets/img/slider-layer-2.png differ diff --git a/assets/img/thumbnail-lg.svg b/assets/img/thumbnail-lg.svg new file mode 100644 index 00000000..526ba12f --- /dev/null +++ b/assets/img/thumbnail-lg.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/assets/img/thumbnail-null.svg b/assets/img/thumbnail-null.svg new file mode 100644 index 00000000..0921b9ed --- /dev/null +++ b/assets/img/thumbnail-null.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/thumbnail-sm.svg b/assets/img/thumbnail-sm.svg new file mode 100644 index 00000000..d5440a8c --- /dev/null +++ b/assets/img/thumbnail-sm.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/assets/img/thumbnail.svg b/assets/img/thumbnail.svg new file mode 100644 index 00000000..6b245645 --- /dev/null +++ b/assets/img/thumbnail.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/assets/img/user_t.jpg b/assets/img/user_t.jpg new file mode 100644 index 00000000..dc5f8534 Binary files /dev/null and b/assets/img/user_t.jpg differ 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']; + } + } + ?> + + + */ + 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/Core/SetupPart1.php b/src/AKSRT/Core/SetupPart1.php index f721a3ec..a99dfc97 100644 --- a/src/AKSRT/Core/SetupPart1.php +++ b/src/AKSRT/Core/SetupPart1.php @@ -428,5 +428,75 @@ trait SetupPart1 { 'after_title' => $title_after, ]); } + + // ── Per-page dynamic sidebars (zibll zib_page_register_sidebar parity) ── + // Pages that opt in via meta `widgets_register=1` + `widgets_register_container` + // (a serialized list of area keys) get page_{key}_{ID} sidebars registered, and + // the matching `page_sidebar_{ID}` is treated as active when any of its areas + // hold widgets. page.php reads page_top_fluid_/page_top_content_/etc. + $this->register_page_sidebars(); + } + + /** + * Register dynamic per-page widget areas (aligns Zibll zib_page_register_sidebar). + * + * Zibll stores, per page, a `widgets_register` flag and a `widgets_register_container` + * serialized array of area keys (sidebar/top_fluid/top_content/bottom_content/bottom_fluid). + * We replicate that: for every page opted in, register page_{key}_{ID} sidebars. + */ + public function register_page_sidebars(): void { + global $wpdb; + // Cache the opt-in page list (postmeta-driven), mirroring zib_get_widgets_register_pages. + $pages = wp_cache_get('aksrt_widgets_register_pages', 'aksrt_cache_group', true); + if (false === $pages) { + // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $rows = $wpdb->get_results( + "SELECT p.ID, p.post_title, pm.meta_value + FROM {$wpdb->posts} p + INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID + WHERE p.post_type = 'page' + AND p.post_status = 'publish' + AND pm.meta_key = 'widgets_register_container' + AND pm.meta_value LIKE '%;}'" + ); + $pages = is_array($rows) ? $rows : []; + wp_cache_set('aksrt_widgets_register_pages', $pages, 'aksrt_cache_group'); + } + + $area_labels = [ + 'sidebar' => __('侧边栏', 'aksrt'), + 'top_fluid' => __('顶部全宽度', 'aksrt'), + 'top_content' => __('主内容上面', 'aksrt'), + 'bottom_content' => __('主内容下面', 'aksrt'), + 'bottom_fluid' => __('底部全宽度', 'aksrt'), + ]; + + $fluid_wrapper_before = '
'; + $card_wrapper_before = '
'; + $title_before = '

'; + $title_after = '

'; + + foreach ($pages as $page) { + $meta_value = maybe_unserialize($page->meta_value); + if (!is_array($meta_value)) { + continue; + } + $name = function_exists('aksrt_str_cut') ? aksrt_str_cut($page->post_title, 0, 10, '...') : mb_substr((string) $page->post_title, 0, 10); + foreach ($meta_value as $key) { + if (!isset($area_labels[$key])) { + continue; + } + $id = 'page_' . $key . '_' . (int) $page->ID; + register_sidebar([ + 'name' => sprintf(__('[页面:%1$s]-%2$s', 'aksrt'), $name, $area_labels[$key]), + 'id' => $id, + 'description' => sprintf(__('显示在[页面:%1$s]的 %2$s 位置', 'aksrt'), $name, $area_labels[$key]), + 'before_widget' => $key === 'sidebar' ? $card_wrapper_before : $fluid_wrapper_before, + 'after_widget' => '
', + 'before_title' => $title_before, + 'after_title' => $title_after, + ]); + } + } } } diff --git a/src/AKSRT/Core/WidgetAjax.php b/src/AKSRT/Core/WidgetAjax.php index aa293aee..3c013bb7 100644 --- a/src/AKSRT/Core/WidgetAjax.php +++ b/src/AKSRT/Core/WidgetAjax.php @@ -59,6 +59,7 @@ class WidgetAjax { $instance = [ 'count' => max(1, min(30, (int) ($request->get_param('count') ?? 6))), 'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'date')), + 'order' => strtoupper(sanitize_key((string) ($request->get_param('order') ?? 'desc'))) === 'ASC' ? 'ASC' : 'DESC', 'cat' => max(0, (int) ($request->get_param('cat') ?? 0)), 'limit_day' => max(0, (int) ($request->get_param('limit_day') ?? 0)), 'topics' => sanitize_text_field((string) ($request->get_param('topics') ?? '')), @@ -91,9 +92,10 @@ class WidgetAjax { 'title' => '', 'category_id' => max(0, (int) ($request->get_param('cat') ?? $request->get_param('category_id') ?? 0)), 'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'date')), + 'order' => strtoupper(sanitize_key((string) ($request->get_param('order') ?? 'desc'))) === 'ASC' ? 'ASC' : 'DESC', 'count' => max(0, min(20, (int) ($request->get_param('tab_count') ?? 0))), 'enabled' => true, - + 'topics' => sanitize_text_field((string) ($request->get_param('topics') ?? '')), 'limit_day' => max(0, (int) ($request->get_param('limit_day') ?? 0)), 'pay_type' => sanitize_key((string) ($request->get_param('pay_type') ?? '')),]; @@ -202,11 +204,12 @@ class WidgetAjax { $paginate = 'ajax'; } $instance = [ - 'count' => max(1, min(20, (int) ($request->get_param('count') ?? 5))), - 'order_by' => sanitize_key((string) ($request->get_param('order_by') ?? 'views')), - 'days' => max(0, (int) ($request->get_param('days') ?? 30)), - 'show_thumb' => $request->get_param('show_thumb') === null ? 1 : (int) $request->get_param('show_thumb'), - 'paginate' => $paginate, + 'count' => max(1, min(20, (int) ($request->get_param('count') ?? 5))), + 'order_by' => sanitize_key((string) ($request->get_param('order_by') ?? 'views')), + 'days' => max(0, (int) ($request->get_param('days') ?? 30)), + 'show_thumb' => $request->get_param('show_thumb') === null ? 1 : (int) $request->get_param('show_thumb'), + 'target_blank'=> (int) ($request->get_param('target_blank') ?? 0), + 'paginate' => $paginate, ]; $page = max(1, (int) ($request->get_param('page') ?? 1)); return $this->payload(\AKSRT\Widgets\HotPostsWidget::query_payload($instance, $page), $page); @@ -220,6 +223,7 @@ class WidgetAjax { 'count' => max(1, min(20, (int) ($request->get_param('count') ?? 8))), 'style' => sanitize_key((string) ($request->get_param('style') ?? 'card')), 'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'date')), + 'order' => strtoupper(sanitize_key((string) ($request->get_param('order') ?? 'desc'))) === 'ASC' ? 'ASC' : 'DESC', 'cat' => max(0, (int) ($request->get_param('cat') ?? 0)), 'limit_day' => max(0, (int) ($request->get_param('limit_day') ?? 0)), 'topics' => sanitize_text_field((string) ($request->get_param('topics') ?? '')), @@ -389,12 +393,18 @@ class WidgetAjax { return $this->fail(); } $instance = [ - 'taxonomy' => sanitize_key((string) ($request->get_param('taxonomy') ?? 'category')), - 'number' => max(1, min(30, (int) ($request->get_param('number') ?? 8))), - 'columns' => max(1, min(4, (int) ($request->get_param('columns') ?? 2))), - 'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'count')), - 'include' => sanitize_text_field((string) ($request->get_param('include') ?? '')), - 'hide_empty' => $request->get_param('hide_empty') === null ? 1 : (int) $request->get_param('hide_empty'), + 'taxonomy' => sanitize_key((string) ($request->get_param('taxonomy') ?? 'category')), + 'number' => max(1, min(30, (int) ($request->get_param('number') ?? 8))), + 'pc_row' => max(1, min(12, (int) ($request->get_param('pc_row') ?? $request->get_param('columns') ?? 2))), + 'm_row' => max(1, min(12, (int) ($request->get_param('m_row') ?? 1))), + 'columns' => max(1, min(12, (int) ($request->get_param('columns') ?? $request->get_param('pc_row') ?? 2))), + 'type' => sanitize_key((string) ($request->get_param('type') ?? 'style-1')), + 'height_scale' => max(20, min(300, (int) ($request->get_param('height_scale') ?? 60))), + 'mask_opacity' => max(0, min(90, (int) ($request->get_param('mask_opacity') ?? 10))), + 'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'count')), + 'include' => sanitize_text_field((string) ($request->get_param('include') ?? '')), + 'hide_empty' => $request->get_param('hide_empty') === null ? 1 : (int) $request->get_param('hide_empty'), + 'target_blank' => (int) ($request->get_param('target_blank') ?? 0), ]; $html = \AKSRT\Widgets\WidgetTermCards::render_html($instance); return new \WP_REST_Response(['success' => true, 'html' => $html], 200); @@ -405,12 +415,14 @@ class WidgetAjax { return $this->fail(); } $instance = [ - 'taxonomy' => sanitize_key((string) ($request->get_param('taxonomy') ?? 'category')), - 'term_ids' => sanitize_text_field((string) ($request->get_param('term_ids') ?? '')), - 'count' => max(1, min(12, (int) ($request->get_param('count') ?? 4))), - 'columns' => max(1, min(3, (int) ($request->get_param('columns') ?? 1))), - 'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'date')), - 'auto_terms' => max(1, min(4, (int) ($request->get_param('auto_terms') ?? 2))), + 'taxonomy' => sanitize_key((string) ($request->get_param('taxonomy') ?? 'category')), + 'term_ids' => sanitize_text_field((string) ($request->get_param('term_ids') ?? '')), + 'count' => max(1, min(20, (int) ($request->get_param('count') ?? 4))), + 'columns' => max(1, min(3, (int) ($request->get_param('columns') ?? 1))), + 'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'date')), + 'order' => strtoupper(sanitize_key((string) ($request->get_param('order') ?? 'desc'))) === 'ASC' ? 'ASC' : 'DESC', + 'auto_terms' => max(1, min(4, (int) ($request->get_param('auto_terms') ?? 2))), + 'target_blank' => (int) ($request->get_param('target_blank') ?? 0), ]; $html = \AKSRT\Widgets\WidgetTermAggregation::render_html($instance); return new \WP_REST_Response(['success' => true, 'html' => $html], 200); 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..1b000775 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,18 +249,22 @@ 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, 'hollow' => !empty($new_instance['hollow']) ? 1 : 0, 'go_link' => !empty($new_instance['go_link']) ? 1 : 0, 'gap' => max(4, min(30, (int) ($new_instance['gap'] ?? 12))), - 'pc_gap' => max(0, min(40, (int) ($new_instance['pc_gap'] ?? $new_instance['gap'] ?? 12))), - 'm_gap' => max(0, min(30, (int) ($new_instance['m_gap'] ?? 6))), + 'pc_gap' => max(0, min(60, (int) ($new_instance['pc_gap'] ?? $new_instance['gap'] ?? 12))), + 'm_gap' => max(0, min(60, (int) ($new_instance['m_gap'] ?? 6))), + 'pc_padding' => in_array(($new_instance['pc_padding'] ?? 'large'), ['small', 'medium', 'large'], true) ? $new_instance['pc_padding'] : 'large', + 'm_padding' => in_array(($new_instance['m_padding'] ?? 'medium'), ['small', 'medium', 'large'], true) ? $new_instance['m_padding'] : 'medium', + 'pc_width' => max(0, min(500, (int) ($new_instance['pc_width'] ?? 0))), + 'm_width' => max(0, min(500, (int) ($new_instance['m_width'] ?? 0))), '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..442b2466 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'), [ @@ -148,6 +152,20 @@ class FriendLinksWidget extends \WP_Widget {

+

+ +



@@ -155,7 +173,8 @@ class FriendLinksWidget extends \WP_Widget {

render_common_form_section($instance); +} /** * @param array $new_instance @@ -167,7 +186,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 +202,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..69ce081a 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( @@ -107,6 +110,7 @@ class HotPostsWidget extends \WP_Widget { $permalink = get_permalink($pid); $title = get_the_title($pid); $time_ago = human_time_diff(get_the_time('U'), current_time('timestamp')) . __('前', 'aksrt'); + $tgt = !empty($instance['target_blank']) ? ' target="_blank" rel="noopener noreferrer"' : ''; // Graphic hero for #1 (zibll zib_hot_posts). if ($index === 1 && $start === 0) { @@ -115,7 +119,7 @@ class HotPostsWidget extends \WP_Widget { $thumb = get_the_post_thumbnail_url($pid, 'medium'); } echo '
'; - echo ''; + echo ''; echo '
'; if ($thumb) { echo ''; @@ -138,12 +142,12 @@ class HotPostsWidget extends \WP_Widget {
- + >
-
+
@@ -173,11 +177,12 @@ class HotPostsWidget extends \WP_Widget { $total = (int) $q->found_posts; $has_more = $page < $max_pages; $base_qs = array_filter([ - 'count' => $count, - 'order_by' => sanitize_key((string) ($instance['order_by'] ?? 'views')), - 'days' => max(0, (int) ($instance['days'] ?? 30)), - 'show_thumb' => !empty($instance['show_thumb']) ? 1 : 0, - 'paginate' => $paginate, + 'count' => $count, + 'order_by' => sanitize_key((string) ($instance['order_by'] ?? 'views')), + 'days' => max(0, (int) ($instance['days'] ?? 30)), + 'show_thumb' => !empty($instance['show_thumb']) ? 1 : 0, + 'target_blank'=> !empty($instance['target_blank']) ? 1 : 0, + 'paginate' => $paginate, ], static function ($v) { return $v !== '' && $v !== null; }); @@ -204,12 +209,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 ''; @@ -247,7 +252,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, + 'target_blank' => !empty($new_instance['target_blank']) ? 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..22349103 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'), [ @@ -21,7 +24,7 @@ class RecentCommentsWidget extends \WP_Widget { * @param array $instance */ public static function render_html(array $instance): string { - $count = max(1, min(20, (int) ($instance['count'] ?? $instance['limit'] ?? 5))); + $count = max(1, min(50, (int) ($instance['count'] ?? $instance['limit'] ?? 5))); $show_avatar = !isset($instance['show_avatar']) || !empty($instance['show_avatar']); // zibll: outpost = post__not_in (multi-ID), outer = author__not_in (multi-ID). // Also keep optional keyword filter via outer_keywords for legacy installs. @@ -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 @@ -192,7 +195,7 @@ class RecentCommentsWidget extends \WP_Widget {

@@ -209,9 +212,7 @@ class RecentCommentsWidget extends \WP_Widget {

-

- -

+ render_common_form_section($instance); ?> */ public function update($new_instance, $old_instance): array { - return [ - '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'] ?? '')), - ]; + return $this->update_common_fields($new_instance, $old_instance, [ + 'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'count' => max(1, min(50, (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'] ?? '')), + 'outer_keywords' => sanitize_text_field((string) ($new_instance['outer_keywords'] ?? '')), + ]); } } 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); ?> -